类型标注是 Python 在「动态类型」与「工程可维护性」之间的折中:它不改变运行时行为,却让 IDE、类型检查器(mypy / pyright)和读者都能受益。它的语法核心很简单–var: type,但围绕冒号右边那个 type 的写法和它何时被求值,这几年演进得很快。

变量标注:var: type 的本质

PEP 526(Python 3.6)引入了变量标注语法:

1
2
x: int = 5            # 标注 + 赋值
name: str # 仅标注,不赋值(不会创建变量 name)

它的本质是给名字贴一张「类型标签」。注解会被收集到 __annotations__ 字典里,运行时不做任何强制:

1
2
3
x: int = "hello"      # 运行时不报错,类型检查器才会警告

print(__annotations__) # {'x': int, 'name': str}

类与实例变量同样支持:

1
2
3
4
5
6
class Node:
value: int = 0 # 类变量标注
def __init__(self) -> None:
self.next: Node = None # 实例变量标注

print(Node.__annotations__) # {'value': int, 'next': Node}

注意 name: str 这种「只标注不赋值」的形式不会真正创建变量,它只是注册了一条注解。

内置泛型与联合类型(3.9 / 3.10)

冒号右边能放的东西,随着版本演进越来越简洁。

内置泛型小写(3.9,PEP 585):容器类型直接支持下标,不再需要 typing.List 这类大写别名。

1
2
3
4
5
6
7
8
9
10
from typing import List, Dict

# 旧写法
xs: List[int] = []
d: Dict[str, int] = {}

# 新写法:直接用内置类型
xs: list[int] = []
d: dict[str, int] = {}
t: tuple[int, str] = (1, "a")

联合类型用 |(3.10,PEP 604):替代 UnionOptional

1
2
3
4
5
6
7
8
9
from typing import Union, Optional

# 旧写法
x: Optional[int] = None
f: Union[Callable[[], int], None] = None

# 新写法:用 | 连接
x: int | None = None
f: Callable[[], int] | None = None

X | Y 是真正的表达式,能在任意 : type 位置使用,甚至运行时也能求值:

1
print(int | str)  # int | str

type 别名语句(3.12)

PEP 695 引入了 type 语句,让类型别名成为一等公民。

1
2
3
4
5
6
7
8
# 旧写法:赋值式别名,弱、不支持递归
Vector = list[float]
Json = dict[str, 'Json'] # 递归必须加引号

# 新写法:type 语句
type Vector = list[float]
type Json = dict[str, Json] # 递归别名,无需引号
type Tree[T] = T | tuple[Tree[T], Tree[T]] # 带类型参数

type 语句定义的别名天然是延迟求值的,因此递归引用不需要字符串,也支持参数化。在 : type 位置直接使用即可:

1
2
v: Vector = [1.0, 2.0]
data: Json = {"key": {"nested": [1, 2]}}

泛型的新语法(3.12)

同样是 PEP 695,泛型函数与类有了方括号语法,不再需要手动声明 TypeVar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from typing import TypeVar, Generic

# 旧写法
T = TypeVar('T')
def first_old(xs: list[T]) -> T: ...
class BoxOld(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value

# 新写法:方括号语法
def first[T](xs: list[T]) -> T: ...
class Box[T]:
def __init__(self, value: T) -> None:
self.value = value

类型参数也可以加约束:

1
2
3
4
5
# 约束为 int 或 str
def add[T: (int, str)](a: T, b: T) -> T: ...

# 上界约束
def process[T: int](x: T) -> T: ...

延迟求值(3.14,PEP 649 / 749)

这是截至目前最根本的变化。注解默认不再在定义时求值,而是惰性求值:模块/类会生成一个 __annotate__ 闭包,注解被存进去,真正用到时才计算。

最直接的好处是–前向引用终于不用写字符串了:

1
2
3
4
5
6
7
8
# 3.13 及以前:前向引用要么加引号,要么 from __future__ import annotations
class Node:
next: "Node | None"

# 3.14+:直接写,无需引号、无需 future import
class Node:
value: int
next: Node | None

这也意味着注解里可以引用尚未导入的名字、包含有副作用的表达式,而不会在模块加载时就把错误抛出来。

运行时想按需取注解,3.14 提供了 annotationlib 模块,支持三种格式:

1
2
3
4
5
6
7
8
9
10
11
import annotationlib

def func(x: int | str) -> None: ...

# SOURCE:原始字符串形式,永不求值
annotationlib.get_annotations(func, format=annotationlib.Format.SOURCE)
# {'x': 'int | str', 'return': 'None'}

# VALUE:求值后的真实对象(默认)
annotationlib.get_annotations(func, format=annotationlib.Format.VALUE)
# {'x': int | str, 'return': NoneType}

FORWARDREF 则介于两者之间:能求值的求值,遇到未定义名字则保留为 ForwardRef

类型收窄:TypeIs(3.13)

PEP 742 引入了 TypeIs,修复了 TypeGuard 的一个长期痛点。

1
2
3
4
5
6
7
from typing import TypeGuard, TypeIs

def is_str_guard(x: object) -> TypeGuard[str]:
return isinstance(x, str)

def is_str_is(x: object) -> TypeIs[str]:
return isinstance(x, str)

关键区别在 else 分支的收窄行为:

1
2
3
4
5
6
7
8
9
def process(x: int | str) -> int:
if is_str_guard(x):
return len(x) # x: str
return x + 1 # x: int | str ← TypeGuard 没收窄,检查器报错

def process(x: int | str) -> int:
if is_str_is(x):
return len(x) # x: str
return x + 1 # x: int ← TypeIs 双向收窄,通过检查

TypeGuard 只收窄 if 分支,else 仍退回原类型;TypeIs 因为要求返回类型是参数类型的子类型,所以能双向收窄。优先用 TypeIs

其他实用新增(3.13)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from typing import ReadOnly, TypedDict, deprecated

# ReadOnly:标记 TypedDict 字段不可变
class Movie(TypedDict):
title: str
year: ReadOnly[int]

# @deprecated:标记弃用,工具会警告调用方
@deprecated("改用 new_func")
def old_func() -> None: ...

# TypedDict 非类型项(PEP 728):允许额外字段
class Config(TypedDict, extra_items=object):
host: str
port: int
# Config 可以包含任意额外的键

常见陷阱

1. 运行时不强制

1
2
x: int = "hello"   # 不报错
x + 1 # 运行时才 TypeError

类型标注是给工具看的契约,不是运行时断言。需要运行时校验用 pydantic / beartype

2. 延迟求值改变了 __annotations__

3.14 之前,__annotations__ 存的是求值后的对象;3.14 之后,直接访问 __annotations__ 可能拿到字符串或 ForwardRef。需要真实类型时用 typing.get_type_hints()annotationlib.get_annotations(..., format=VALUE)

3. 注解不是文档的替代

1
2
3
4
5
6
7
# 坏:注解说了类型,但没说含义
def transfer(amount: float) -> bool: ...

# 好:配合命名与文档,注解才发挥作用
def transfer(amount_in_yuan: float) -> bool:
"""转出指定金额,返回是否成功。"""
...

4. 泛型新语法不等于旧语法完全等价

def f[T]() 创建的 T 是真正的作用域内类型参数,不会泄漏到模块级;而旧 T = TypeVar('T') 是模块级变量,能被多处复用。混用时要小心作用域。

总结

版本特性PEP示例
3.6变量标注526x: int = 5
3.9内置泛型下标585xs: list[int]
3.10联合类型 |604x: int | None
3.12type 别名语句695type Vector = list[float]
3.12泛型方括号语法695def f[T](x: T) -> T
3.13TypeIs 双向收窄742-> TypeIs[str]
3.13ReadOnly / @deprecated705 / 702ReadOnly[int]
3.14注解延迟求值649 / 749前向引用免字符串

类型标注的演进方向很清晰:注解越来越像「写给人看和工具看的元数据」,而不是「模块加载时执行的代码」。3.14 的延迟求值是这条路上的里程碑–它消除了前向引用的字符串包袱,让类型标注终于可以像写文档一样自然地写下去。