> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# mypy 类型系统完全指南
- URL: https://blog.vercanti.com/mypy-lei-xing-xi-tong-wan-quan-zhi-nan/
- Published: 2026-08-28T14:34:34.000Z
- Updated: 2026-08-28T14:56:50.000Z
- Description: 相关文档：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
- Author: yellowdog
- Tags: Python, 基础

> 官方文档：<https://mypy.readthedocs.io/>  
> 适用版本：mypy 1.10（2026-05-07 核实）

相关文档：[Pydantic完全指南](https://blog.vercanti.com/pydantic-wan-quan-zhi-nan/) [FastAPI完全指南](https://blog.vercanti.com/fastapi-wan-quan-zhi-nan/) [装饰器与函数高级](https://blog.vercanti.com/python-zhuang-shi-qi-yu-han-shu-gao-ji-yong-fa/)

---

## 1\. 基础概念

### 为什么用类型注解

Python 是动态类型语言，类型注解（PEP 484+）不影响运行时，但提供：

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

### 安装与基础使用

```bash
pip install mypy

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

# 检查整个包
mypy src/

# 严格模式
mypy --strict src/

```

### mypy.ini / pyproject.toml 配置

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

# 对特定包放宽限制
[mypy-celery.*]
ignore_missing_imports = true

```

```toml
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = true
exclude = ["migrations", "tests"]

```

---

## 2\. 基础类型注解

```python
# 基本类型
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
# 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\. 函数类型

```python
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 — 泛型

```python
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，无需继承：

```python
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）

```python
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 — 字面量类型

```python
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 — 字典类型

```python
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": "a@b.com"}
user["unknown"]  # mypy 报错：TypedDict "UserDict" has no key "unknown"

```

---

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

```python
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）

```python
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 — 函数重载

```python
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 |

### 类型忽略注释

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

```

---

## 12\. 最佳实践

### 渐进式类型注解

不要一次性给整个项目加注解，从公共 API（函数签名）开始，逐步内部化：

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

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

```

### 避免 `Any` 扩散

`Any` 类型会感染（`Any` 的操作结果也是 `Any`），应尽量缩窄范围：

```python
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` 等选项。

```ini
# 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 会在检查报告中输出推断结果，无需运行代码。用完后删除，否则运行时报错。

```python
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]`。

```bash
pip install types-requests types-PyMySQL types-redis

```

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

```yaml
# .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` 保护或提供默认值。

```python
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` 返回值报警。

```python
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` 模块中的类型。

```python
# 方法 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]:
    ...

```

---

## 参见

- [Pydantic完全指南](https://blog.vercanti.com/pydantic-wan-quan-zhi-nan/)
- [FastAPI完全指南](https://blog.vercanti.com/fastapi-wan-quan-zhi-nan/)
- [装饰器与函数高级](https://blog.vercanti.com/python-zhuang-shi-qi-yu-han-shu-gao-ji-yong-fa/)
- [TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/)