类型注解解决什么
Python 是动态类型语言,函数能接收任何类型的参数,这也意味着错误往往到运行那一刻才暴露。类型注解的价值在于把「约定」写进代码:参数是什么类型、返回值是什么类型,人和工具都能看懂。
mypy 这类静态检查工具会在运行之前扫描代码,把类型不匹配的问题直接指出来。类型注解不是给 Python 增加强制类型,而是让动态语言在关键边界上变得可验证,既保留灵活,又降低出错概率。
项目越大、协作者越多,类型注解带来的收益越明显。接口边界、数据模型、公共函数这三类位置最值得优先加注解,内部实现细节反而可以从容一点。
类型注解是写给人的文档,也是写给机器的检查单。
- 注解语法变量与函数签名写法。
- typing容器、可选与联合类型。
- 泛型复用类型逻辑。
- mypy静态检查与配置。
变量与函数基础注解
变量注解用冒号声明类型,函数注解在参数后加冒号、在函数末尾用箭头声明返回值类型。最简单的写法只涉及几个内建类型:int、float、str、bool。
name: str = "吉极云码"
count: int = 42
def greet(name: str, times: int = 1) -> str:
return (name + "!") * times
函数注解尤其重要,它是模块对外接口的契约。团队协作时,别人看到签名就知道该传什么、会得到什么,不用钻进函数体里读实现。
要注意注解只是声明,不会在运行时强制校验。类型检查要靠 mypy 这类工具在开发阶段完成,运行时如果确实需要校验,再配合 Pydantic 这类库做数据验证。
容器、可选与联合类型
真实代码里更多的是列表、字典、可能为空的返回值。typing 模块提供了对应工具:List、Dict、Optional、Union,Python 3.10 之后还能用内建的 list、dict 加泛型语法。
from typing import Optional
scores: list[int] = [90, 85, 78]
lookup: dict[str, int] = {"a": 1, "b": 2}
def find_user(uid: int) -> Optional[str]:
return "用户" if uid > 0 else None
Optional[str] 等价于 str 或 None,明确告诉调用方返回值可能为空,调用前必须先判断,否则 mypy 会报错。联合类型 Union[str, int] 在 3.10 之后可直接写作 str | int,代码更简洁。
| 写法 | 含义 |
|---|---|
| list[int] | 整数列表 |
| dict[str, int] | 键为字符串、值为整数的字典 |
| Optional[str] | 字符串或 None |
| str | int | 字符串或整数 |
泛型与协议
有些函数不在乎容器里装的是什么类型,只在乎容器本身的操作。这时候用泛型把类型参数化,函数就能复用到任意类型上,同时保留类型信息。
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
value = first([1, 2, 3]) # 推断为 int
协议(Protocol)则定义结构上的要求:只要对象有协议要求的属性和方法,就视为满足该类型。这让类型系统贴近「鸭子类型」的直觉,同时把结构契约写进检查里。
泛型和协议是进阶能力,先把基础注解用熟练,再逐步引入这两样,避免一上来把代码写得难以读懂。
mypy 接入与配置
mypy 是最流行的 Python 静态类型检查器。安装后直接运行,它读取注解并按配置的严格程度检查代码。建议在项目根放一个配置文件,把严格级别和忽略规则集中管理。
[mypy]
python_version = 3.12
strict = true
ignore_missing_imports = true
warn_unused_ignores = true
strict 会开启一整套严格检查:变量必须有类型、参数必须有注解、返回值必须标注。一开始太严格会劝退团队,建议从基础配置起步,在 CI 里逐步提高要求。
把 mypy 放进 CI 流水线,作为合并请求的检查门槛,类型问题就不会流进主干。配合编辑器插件,写代码时就能实时看到类型错误,反馈成本最低。
存量代码迁移策略
给一个已经跑了几年的项目全量补类型注解不现实,也不必要。推荐按风险从高到低迁移:先给公共 API、数据模型、外部接口层补注解,这些地方错了影响面最大;内部实现细节可以慢慢补。
# 迁移第一步:只标注入口与出口
def create_order(user_id: int, items: list[dict]) -> dict:
pass # 入口与出口先标注,内部稍后补充
# 迁移第二步:内部变量逐步补注解
def create_order(user_id: int, items: list[dict]) -> dict:
total: float = 0.0
for item in items:
total += item["price"]
return {"total": total}
mypy 提供 per-module 配置,可以先对核心模块开启严格检查,其他模块保持宽松。每次重构顺手补一片,几个月下来整个项目的类型覆盖度就能显著提升。
类型安全实践清单
- 公共函数、数据模型、接口边界优先加注解。
- 返回值可能为空时用 Optional 或联合类型标清楚。
- 容器类型写清楚元素类型,不要裸写 list。
- mypy 接入 CI,作为合并门槛。
- 迁移存量代码按风险排序,从入口和出口开始。
- 类型检查通过后,运行时数据校验交给 Pydantic。
类型注解做得越早,重构时越敢动手。