Python 类型标注演进
类型标注是 Python 在「动态类型」与「工程可维护性」之间的折中:它不改变运行时行为,却让 IDE、类型检查器(mypy / pyright)和读者都能受益。它的语法核心很简单–var: type,但围绕冒号右边那个 type 的写法和它何时被求值,这几年演进得很快。
变量标注:var: type 的本质
PEP 526(Python 3.6)引入了变量标注语法:
1 | |
它的本质是给名字贴一张「类型标签」。注解会被收集到 __annotations__ 字典里,运行时不做任何强制:
1 | |
类与实例变量同样支持:
1 | |
注意 name: str 这种「只标注不赋值」的形式不会真正创建变量,它只是注册了一条注解。
内置泛型与联合类型(3.9 / 3.10)
冒号右边能放的东西,随着版本演进越来越简洁。
内置泛型小写(3.9,PEP 585):容器类型直接支持下标,不再需要 typing.List 这类大写别名。
1 | |
联合类型用 |(3.10,PEP 604):替代 Union 与 Optional。
1 | |
X | Y 是真正的表达式,能在任意 : type 位置使用,甚至运行时也能求值:
1 | |
type 别名语句(3.12)
PEP 695 引入了 type 语句,让类型别名成为一等公民。
1 | |
type 语句定义的别名天然是延迟求值的,因此递归引用不需要字符串,也支持参数化。在 : type 位置直接使用即可:
1 | |
泛型的新语法(3.12)
同样是 PEP 695,泛型函数与类有了方括号语法,不再需要手动声明 TypeVar。
1 | |
类型参数也可以加约束:
1 | |
延迟求值(3.14,PEP 649 / 749)
这是截至目前最根本的变化。注解默认不再在定义时求值,而是惰性求值:模块/类会生成一个 __annotate__ 闭包,注解被存进去,真正用到时才计算。
最直接的好处是–前向引用终于不用写字符串了:
1 | |
这也意味着注解里可以引用尚未导入的名字、包含有副作用的表达式,而不会在模块加载时就把错误抛出来。
运行时想按需取注解,3.14 提供了 annotationlib 模块,支持三种格式:
1 | |
FORWARDREF 则介于两者之间:能求值的求值,遇到未定义名字则保留为 ForwardRef。
类型收窄:TypeIs(3.13)
PEP 742 引入了 TypeIs,修复了 TypeGuard 的一个长期痛点。
1 | |
关键区别在 else 分支的收窄行为:
1 | |
TypeGuard 只收窄 if 分支,else 仍退回原类型;TypeIs 因为要求返回类型是参数类型的子类型,所以能双向收窄。优先用 TypeIs。
其他实用新增(3.13)
1 | |
常见陷阱
1. 运行时不强制
1 | |
类型标注是给工具看的契约,不是运行时断言。需要运行时校验用 pydantic / beartype。
2. 延迟求值改变了 __annotations__
3.14 之前,__annotations__ 存的是求值后的对象;3.14 之后,直接访问 __annotations__ 可能拿到字符串或 ForwardRef。需要真实类型时用 typing.get_type_hints() 或 annotationlib.get_annotations(..., format=VALUE)。
3. 注解不是文档的替代
1 | |
4. 泛型新语法不等于旧语法完全等价
def f[T]() 创建的 T 是真正的作用域内类型参数,不会泄漏到模块级;而旧 T = TypeVar('T') 是模块级变量,能被多处复用。混用时要小心作用域。
总结
| 版本 | 特性 | PEP | 示例 |
|---|---|---|---|
| 3.6 | 变量标注 | 526 | x: int = 5 |
| 3.9 | 内置泛型下标 | 585 | xs: list[int] |
| 3.10 | 联合类型 | | 604 | x: int | None |
| 3.12 | type 别名语句 | 695 | type Vector = list[float] |
| 3.12 | 泛型方括号语法 | 695 | def f[T](x: T) -> T |
| 3.13 | TypeIs 双向收窄 | 742 | -> TypeIs[str] |
| 3.13 | ReadOnly / @deprecated | 705 / 702 | ReadOnly[int] |
| 3.14 | 注解延迟求值 | 649 / 749 | 前向引用免字符串 |
类型标注的演进方向很清晰:注解越来越像「写给人看和工具看的元数据」,而不是「模块加载时执行的代码」。3.14 的延迟求值是这条路上的里程碑–它消除了前向引用的字符串包袱,让类型标注终于可以像写文档一样自然地写下去。

