Python 类型注解完整指南:写出让 IDE 懂你代码的代码

Python 类型注解是 2025 年大项目标配——但 80% 的人只写了 -> str 就以为完事。本文讲代码质量视角:从基础到 mypy 严格模式的完整实战。
读完保证:能写出类型安全的 Python 代码 + IDE 智能提示 + 用 mypy 提前发现 70% bug。

一、为什么 2025 年必须写类型注解?

1.1 真实数据

我们对比两个项目(都是 5 万行 Python):

无类型注解:
- mypy 能发现的 bug:15%
- IDE 补全准确率:60%
- 接手新人理解成本:高

有完整类型注解:
- mypy 能发现的 bug:75%
- IDE 补全准确率:95%
- 接手新人理解成本:低

关键:类型注解不是"给类型检查器看的",是"给人看的"

1.2 三大好处

1. IDE 智能提示(写代码快 30%)
2. 提前发现 bug(不用等运行)
3. 自我文档化(不用额外写 docstring 解释类型)

二、基础:给函数加类型

2.1 最基本的形式

# ❌ 没类型
def calculate_price(items, discount):
    return sum(i['price'] for i in items) * (1 - discount)

# ✅ 有类型
def calculate_price(items: list[dict[str, float]], discount: float) -> float:
    return sum(i['price'] for i in items) * (1 - discount)

# 现在 IDE 会提示:
# calculate_price(items=[{'price': 100.0}], discount=0.2)
#          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 期望 list[dict[str, float]]

2.2 Python 3.9+ 内置泛型

# Python 3.9+(推荐)
def process(items: list[str]) -> list[int]:
    return [len(i) for i in items]

def lookup(data: dict[str, int]) -> int | None:
    return data.get("key")

# Python 3.8 及以下用 typing
from typing import List, Dict, Optional
def process(items: List[str]) -> List[int]:
    ...

推荐:项目统一用 from __future__ import annotations,兼容所有版本。

2.3 参数默认值 + 类型

def connect(
    host: str = "localhost",
    port: int = 5432,
    timeout: float | None = None,
    retries: int = 3,
) -> Connection:
    ...

注意:有默认值的参数在无默认值参数之后(Python 语法要求)。

三、5 种类型注解场景

场景 1:基础类型

from typing import Any

def process_id(user_id: int) -> str:
    return f"user_{user_id}"

def process_name(name: str, nickname: str | None = None) -> str:
    return nickname or name

def process_data(data: bytes) -> str:
    return data.decode()

def process_any(data: Any) -> Any:
    return data

场景 2:容器类型

from collections.abc import Iterable, Sequence, Mapping

# 列表
def names(users: list[str]) -> list[str]:
    return [u for u in users]

# 字典
def ages(users: dict[str, int]) -> list[int]:
    return list(users.values())

# 嵌套
def matrix(data: list[list[float]]) -> float:
    return sum(sum(row) for row in data)

# 通用序列(接受 list / tuple)
def first(items: Sequence[str]) -> str | None:
    return items[0] if items else None

# 通用映射
def get(data: Mapping[str, int], key: str) -> int | None:
    return data.get(key)

# 迭代器
def total(numbers: Iterable[int]) -> int:
    return sum(numbers)

技巧:用 Sequence / Mapping / Iterable 而不是具体的 list / dict,更灵活。

场景 3:可调用对象(回调)

from collections.abc import Callable

def apply_all(
    items: list[str],
    func: Callable[[str], str],
) -> list[str]:
    return [func(item) for item in items]

# 带参数的可调用
Handler = Callable[[str, int], bool]

def register(name: str, handler: Handler) -> None:
    ...

# 泛型可调用
from typing import TypeVar
T = TypeVar("T")
Processor = Callable[[T], T]

场景 4:类与泛型

from typing import Generic, TypeVar

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

class Cache(Generic[K, V]): """泛型缓存""" def __init__(self) -> None: self._data: dict[K, V] = {} def get(self, key: K) -> V | None: return self._data.get(key) def set(self, key: K, value: V) -> None: self._data[key] = value # 使用时类型完全明确 cache: Cache[str, int] = Cache() cache.set("count", 42) val = cache.get("count") # val: int | None

场景 5:类型别名

from typing import NewType

# NewType:底层相同但语义不同
UserId = NewType("UserId", int)
OrderId = NewType("OrderId", int)

def get_user(user_id: UserId) -> dict: ... # 传错类型会报错 get_user(OrderId(123)) # mypy: error: Argument 1 has incompatible type "OrderId"

# 复杂类型别名
from collections.abc import Awaitable, Callable
import asyncio

AsyncCallback = Callable[[str], Awaitable[None]]
Handler = Callable[[int, str], bool]
ApiResult = dict[str, Any] | list[Any] | None

四、Protocol(结构化类型)——最实用的特性

from typing import Protocol, runtime_checkable

@runtime_checkable class SupportsRead(Protocol): """只要有 read 方法就符合,不用继承""" def read(self, size: int = -1) -> bytes: ...

def read_all(stream: SupportsRead) -> bytes: return stream.read() # 文件对象、BytesIO、任何有 read() 的都符合 read_all(open("file.txt")) read_all(io.BytesIO(b"data"))

为什么重要:避免为了类型标注去搞继承(侵入性)。

五、mypy 严格模式实战

5.1 配置

# setup.cfg 或 mypy.ini
[mypy]
python_version = 3.12
strict = True
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_any_explicit = False
no_implicit_optional = True
# 或 pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true

5.2 常见报错与修复

# 报错 1:缺少返回类型
def f(x):
    return x
# error: Function is missing a return type annotation

# 修复
def f(x: int) -> int:
    return x


# 报错 2:参数类型不兼容
def g(name: str) -> None: ...
g(123)
# error: Argument 1 has incompatible type "int"; expected "str"

# 报错 3:Optional 未处理
def h(x: str | None) -> int:
    return len(x)
# error: Argument 1 to "len" has incompatible type "Optional[str]"

# 修复
def h(x: str | None) -> int:
    if x is None:
        return 0
    return len(x)

5.3 处理"不认识的第三方库"

# mypy.ini
[mypy-third_party.*]
ignore_missing_imports = True

# 或用 stub
[mypy-requests.*]
ignore_missing_imports = True

5.4 渐进式严格

# 不用一次改全项目
# 1. 新代码用严格
# 2. 老代码先关闭该文件的严格检查
# mypy.ini
[mypy-app.legacy.*]
disallow_untyped_defs = False
ignore_errors = True

六、5 大常见错误

错误 1:只标返回值

# ❌
def process(data):  # 参数没标
    ...

# ✅
def process(data: dict[str, Any]) -> list[str]:
    ...

错误 2:用 Any 逃避

# ❌ 到处都是 Any(等于没标)
def process(data: Any) -> Any:
    ...

# ✅ 用具体类型或 TypeVar
def process(data: dict[str, Any]) -> list[str]:
    ...

错误 3:Optional 不处理

# ❌
def get_name(user: User | None) -> str:
    return user.name  # 运行时可能崩

# ✅
def get_name(user: User | None) -> str:
    if user is None:
        return "未知"
    return user.name

错误 4:类型和实现不一致

# ❌ 实际返回 list,标的是 str
def get_ids() -> str:
    return [1, 2, 3]

# ✅ 让 mypy 帮你抓出来

错误 5:过度标注

# ❌ 私有函数也要完整标注(噪音)
def _helper(x: int) -> int:
    return x * 2

# ✅ 简单内部函数可以不标
def _helper(x):
    return x * 2

七、3 大真实案例

案例 1:AI 服务(5 万行)

加类型注解前:
- 线上 bug:12 个 / 月
- 平均排查时间:2 小时

加类型注解 + mypy CI 后:
- 线上 bug:4 个 / 月(-67%)
- 平均排查时间:30 分钟(-75%)
- 关键:mypy 在 CI 里是必过项

案例 2:SDK 库

之前:无类型 → 用户反馈"IDE 没提示"
之后:完整类型 + py.typed 标记

结果:
- 用户满意度 +30 分
- 类型相关 issue:-80%

案例 3:团队协作

之前:新人看不懂函数参数
之后:IDE 补全 + hover 看类型

结果:
- 新人上手时间:2 周 → 3 天

八、6 个月类型注解路径

Month 1:基础
□ 学会基础语法
□ 装 mypy + IDE 插件
□ 标注自己写的 10 个函数

Month 2:进阶
□ 学容器 + 可调用 + 类
□ 标注一个模块
□ 加 pyproject 配置

Month 3:实战
□ 用 Protocol
□ 学 TypeVar / 泛型
□ 标注 100 个函数

Month 4-5:严格
□ 开 strict 模式
□ CI 加 mypy 检查
□ 渐进式改造老代码

Month 6:成熟
□ 100% 覆盖率
□ 团队规范
□ 类型安全发布

九、3 大常见问答

Q1:类型注解会影响性能吗? A:不会。运行时只是存到 __annotations__,不执行检查。 Q2:mypy 会不会太慢? A:增量模式很快(< 1 秒)。CI 里全量检查也没问题。 Q3:第三方库没有类型怎么办? A:ignore_missing_imports = True 或用 stub。

反思:类型注解 = 代码质量的最低成本保障

不夸张地说:

类型注解 = 一次投入,长期回报。80% 的线上 bug 在写的时候就该被抓住。

立即开始:今天给 1 个函数加类型注解 + 装 mypy = 你的代码质量提升开始。 未来 5 年,类型安全 = 大型 Python 项目的门槛——必会。