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 语法写注解,运行时报 TypeError 或 NameError。
原因: 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]:
...