mypy 类型系统完全指南

相关文档:Pydantic完全指南(/pydantic-wan-quan-zhi-nan/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) 装饰器与函数高级(/python-zhuang-shi-qi-yu-han-shu-gao-ji-yong-fa/) Python 是动态类型语言,类型注解(PEP 484+)不影响运行时,但提供: Protocol 定义一组方法/属性的接口,只要对象实现了这些方法,就满足 Protocol,无需继承: 不要一次性给整个项目加注解,从公共 API(函数签名)开始,逐步内部化: Any

分享

官方文档:https://mypy.readthedocs.io/
适用版本:mypy 1.10(2026-05-07 核实)

相关文档:Pydantic完全指南 FastAPI完全指南 装饰器与函数高级


1. 基础概念

为什么用类型注解

Python 是动态类型语言,类型注解(PEP 484+)不影响运行时,但提供:

  • IDE 智能补全和错误提示
  • mypy 静态检查,提前发现 bug
  • 代码即文档(参数类型一目了然)
  • 重构时的安全网

安装与基础使用

pip install mypy

# 检查单个文件
mypy src/main.py

# 检查整个包
mypy src/

# 严格模式
mypy --strict src/

mypy.ini / pyproject.toml 配置

# mypy.ini
[mypy]
python_version = 3.12
strict = true
ignore_missing_imports = true    # 第三方库无 stub 时不报错
exclude = migrations/

# 对特定包放宽限制
[mypy-celery.*]
ignore_missing_imports = true
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = true
exclude = ["migrations", "tests"]

2. 基础类型注解

# 基本类型
x: int = 1
name: str = "Alice"
pi: float = 3.14
flag: bool = True
data: bytes = b"hello"

# None
def greet(name: str) -> None:
    print(f"Hello, {name}")

# 可选类型(可以是 T 或 None)
from typing import Optional

def find_user(user_id: int) -> Optional[str]:  # 旧写法
    ...

def find_user(user_id: int) -> str | None:     # Python 3.10+ 推荐
    ...

集合类型

# Python 3.9+ 可直接用内置类型
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 90}
tags: set[str] = {"python", "mypy"}
point: tuple[int, int] = (1, 2)
mixed: tuple[str, int, bool] = ("Alice", 25, True)
pairs: tuple[int, ...] = (1, 2, 3, 4)   # 可变长度同类型元组

# 旧写法(Python 3.8 及以下)
from typing import List, Dict, Set, Tuple
names: List[str] = []

3. 函数类型

from typing import Callable

# 函数签名注解
def process(items: list[int], fn: Callable[[int], int]) -> list[int]:
    return [fn(x) for x in items]

# 可变参数
def log(*args: str, level: str = "INFO") -> None:
    print(f"[{level}]", *args)

# 关键字参数字典
def create(**kwargs: str) -> dict[str, str]:
    return dict(kwargs)

# 返回 Callable 的函数(装饰器)
from typing import TypeVar
F = TypeVar("F", bound=Callable[..., object])

def decorator(func: F) -> F:
    return func

4. TypeVar — 泛型

from typing import TypeVar, Generic

T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")

# 泛型函数:返回类型与输入类型一致
def first(items: list[T]) -> T:
    return items[0]

result: int = first([1, 2, 3])      # 推断为 int
name: str = first(["a", "b", "c"]) # 推断为 str

# 有上界的 TypeVar(T 必须是 Comparable 的子类型)
from typing import SupportsLessThan

C = TypeVar("C", bound="SupportsLessThan")

def maximum(a: C, b: C) -> C:
    return a if a > b else b

# 泛型类
class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()

    def peek(self) -> T:
        return self._items[-1]

s: Stack[int] = Stack()
s.push(1)
top: int = s.pop()

5. Protocol — 结构子类型(鸭子类型的类型安全版)

Protocol 定义一组方法/属性的接口,只要对象实现了这些方法,就满足 Protocol,无需继承:

from typing import Protocol, runtime_checkable

class Drawable(Protocol):
    def draw(self) -> None: ...

class Circle:
    def draw(self) -> None:
        print("画圆")

class Square:
    def draw(self) -> None:
        print("画方")

# Circle 和 Square 都没有继承 Drawable,但都满足 Protocol
def render(shape: Drawable) -> None:
    shape.draw()

render(Circle())  # OK
render(Square())  # OK

# runtime_checkable:支持 isinstance 检查
@runtime_checkable
class Sized(Protocol):
    def __len__(self) -> int: ...

print(isinstance([1, 2, 3], Sized))  # True

常用内置 Protocol(collections.abc)

from collections.abc import (
    Iterable,       # 可迭代(有 __iter__)
    Iterator,       # 迭代器(有 __next__)
    Sequence,       # 序列(有 __getitem__ 和 __len__)
    Mapping,        # 映射(有 __getitem__、keys、values)
    MutableMapping, # 可变映射
    Callable,       # 可调用
    Awaitable,      # 可 await
    AsyncIterable,  # 异步可迭代
    Generator,      # 生成器
)

def process(items: Iterable[int]) -> list[int]:
    return list(items)

# 接受任何可迭代对象:list, tuple, set, generator...
process([1, 2, 3])
process((1, 2, 3))
process(x for x in range(10))

6. Literal — 字面量类型

from typing import Literal

Direction = Literal["left", "right", "up", "down"]
Status = Literal["pending", "active", "inactive"]
HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"]

def move(direction: Direction) -> None:
    ...

move("left")    # OK
move("wrong")   # mypy 报错:Argument 1 to "move" has incompatible type

def set_status(status: Status) -> None:
    ...

7. TypedDict — 字典类型

from typing import TypedDict, Required, NotRequired

class UserDict(TypedDict):
    id: int
    name: str
    email: str
    age: NotRequired[int]   # 可选键(Python 3.11+)

# 旧写法:total=False 使所有键可选
class PartialUser(TypedDict, total=False):
    name: str
    email: str

user: UserDict = {"id": 1, "name": "Alice", "email": "[email protected]"}
user["unknown"]  # mypy 报错:TypedDict "UserDict" has no key "unknown"

8. ParamSpec — 保留函数签名的装饰器

from typing import ParamSpec, TypeVar, Callable
from functools import wraps
import time

P = ParamSpec("P")
R = TypeVar("R")

def timer(func: Callable[P, R]) -> Callable[P, R]:
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__} 耗时 {time.perf_counter() - start:.3f}s")
        return result
    return wrapper

@timer
def compute(x: int, y: int) -> int:
    return x + y

# mypy 知道 compute 的签名仍然是 (x: int, y: int) -> int
result: int = compute(1, 2)
compute("x", 2)  # mypy 报错:参数类型不匹配

9. 类型守卫(Type Narrowing)

from typing import TypeGuard, Union

def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in val)

def process(items: list[object]) -> None:
    if is_string_list(items):
        # 此处 items 的类型被缩窄为 list[str]
        print(items[0].upper())  # OK,mypy 知道是 str

# isinstance 自动类型缩窄
def handle(value: int | str) -> str:
    if isinstance(value, int):
        return str(value * 2)   # 此处 value 是 int
    return value.upper()        # 此处 value 是 str

# assert 类型缩窄
from typing import assert_never

def handle_status(status: Literal["ok", "error"]) -> str:
    if status == "ok":
        return "成功"
    elif status == "error":
        return "失败"
    else:
        assert_never(status)  # 告诉 mypy 此处不可达,若有遗漏会报错

10. overload — 函数重载

from typing import overload

@overload
def parse(value: str) -> int: ...
@overload
def parse(value: bytes) -> str: ...

def parse(value: str | bytes) -> int | str:
    if isinstance(value, str):
        return int(value)
    return value.decode()

result1: int = parse("42")       # mypy 推断为 int
result2: str = parse(b"hello")   # mypy 推断为 str

11. 常用 mypy 错误及解决

错误 原因 解决
error: Item "None" of "X | None" has no attribute "foo" 可能为 None 未检查 if x is not None: 或用 assert x is not None
error: Incompatible types in assignment 赋值类型不匹配 检查变量声明类型
error: Missing return statement 函数可能不返回值 补全所有分支的 return
error: Need type annotation for "x" 无法推断类型 显式声明 x: list[int] = []
error: Module has no attribute "xxx" 第三方库缺 stub pip install types-xxx 或添加 ignore_missing_imports

类型忽略注释

x = some_dynamic_value()  # type: ignore[assignment]
result = lib.undocumented_method()  # type: ignore[attr-defined]

12. 最佳实践

渐进式类型注解

不要一次性给整个项目加注解,从公共 API(函数签名)开始,逐步内部化:

# 第一步:只注解公共函数签名
def create_user(name: str, email: str) -> dict:
    ...

# 第二步:精化返回类型
def create_user(name: str, email: str) -> UserDict:
    ...

避免 Any 扩散

Any 类型会感染(Any 的操作结果也是 Any),应尽量缩窄范围:

from typing import Any, cast

# 不得不用 Any 时,用 cast 明确目标类型
raw: Any = json.loads(data)
user: UserDict = cast(UserDict, raw)  # 告诉 mypy 相信这是 UserDict

最佳实践

从 strict=False 开始,逐步增加检查严格度:在旧代码库直接开启 --strict 会产生数百个错误,阻碍落地。推荐按模块逐步迁移:先用 --ignore-missing-imports 忽略无类型库,再逐步启用 --disallow-untyped-defs 等选项。

# mypy.ini 渐进策略
[mypy]
ignore_missing_imports = True   # 第一步:只检查已有类型的代码

[mypy-myapp.*]
disallow_untyped_defs = True    # 第二步:强制 myapp 内所有函数有类型注解

[mypy-myapp.api.*]
strict = True                   # 第三步:核心模块启用最严格检查

reveal_type() 临时调试类型推断:不确定某个变量的推断类型时,插入 reveal_type(var),mypy 会在检查报告中输出推断结果,无需运行代码。用完后删除,否则运行时报错。

import numpy as np
arr = np.zeros((3, 3))
reveal_type(arr)  # mypy: Revealed type is "numpy.ndarray[Any, numpy.dtype[numpy.floating[Any]]]"

第三方库缺类型时用 types-xxx 包或 # type: ignore:大多数流行库已有对应的 types-* stub 包,pip install types-requests 等可解决"Missing stubs"错误。实在没有 stub 的库在 import 行加 # type: ignore[import]

pip install types-requests types-PyMySQL types-redis

CI 中集成 mypy 检查,与 pylint/ruff 配合:mypy 做类型检查,ruff/flake8 做风格检查,各司其职。推荐在 pre-commit 和 CI pipeline 中都运行 mypy,防止类型错误合入主干。

# .pre-commit-config.yaml
- repo: https://github.com/pre-commit/mirrors-mypy
  rev: v1.10.0
  hooks:
    - id: mypy
      additional_dependencies: [types-requests]

常见陷阱

陷阱:Optional[X] 和 X | None 的区别被忽视

现象: 函数参数标注为 Optional[str] 但代码中直接使用不检查 None,运行时报 AttributeError: 'NoneType' object has no attribute 'strip'

原因: Optional[str] 等价于 str | None,调用者可以传 None,函数内部必须先判断。mypy 会报错但有些人忽略了 mypy 输出。

解决: 标注 Optional 就必须在函数体内用 if x is not None 保护或提供默认值。

def process(name: str | None) -> str:
    # 错误:name 可能为 None
    return name.strip()

    # 正确
    return (name or "").strip()
    # 或
    if name is None:
        return ""
    return name.strip()

陷阱:Any 类型感染导致检查形同虚设

现象: 文件有 mypy 注解,但实际检查没有发现任何问题,因为核心变量被推断为 Any

原因: 调用了无类型的函数(返回 Any),后续对该变量的所有操作都是 Any,mypy 不再检查。

解决:cast() 显式收窄类型;用 --warn-return-any 标志让 mypy 对 Any 返回值报警。

from typing import cast
import json

raw = json.loads(data)        # type: Any
config = cast(dict[str, str], raw)   # 明确告知 mypy 这是 dict[str, str]

陷阱:类型注解在运行时求值引发 NameError

现象: Python 3.9 以下版本,使用 list[str]X | Y 语法写注解,运行时报 TypeErrorNameError

原因: Python 3.9 以下不支持将内置类型直接用于泛型(需用 List[str]),| 联合类型语法(Python 3.10+)。

解决: 添加 from __future__ import annotations(所有注解延迟求值)或使用 typing 模块中的类型。

# 方法 A:延迟求值(推荐)
from __future__ import annotations

def foo(x: list[str] | None) -> dict[str, int]:
    ...

# 方法 B:用 typing 模块(兼容性最好)
from typing import Dict, List, Optional
def foo(x: Optional[List[str]]) -> Dict[str, int]:
    ...

参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog