本节目标:掌握类型注解的常用语法,理解「注解在运行时不做检查」这一核心事实,并学会用 pyright / mypy 对真实代码做静态检查。
适用版本:Python 3.12+(实测 3.14.6)
9.1 类型注解语法与 pyright / mypy
到第 8 章为止,我们写的所有代码都只靠运行时行为说话。但从这一节开始,我们要给代码加上类型注解(type hints),让工具在代码跑起来之前就发现错误。这是 Python 从「脚本语言」走向「大型工程语言」最关键的一步,也是后面第 14 章的 CI 门禁、第 15 章的打包发布、第 16 章的 FastAPI 全都依赖的地基。
9.1.1 最重要的一句话:注解在运行时不做任何检查
先把结论放在最前面,因为这是绝大多数初学者踩的第一个坑:
类型注解对运行时没有任何强制力。 你写下 a: int,Python 解释器不会因此拒绝传入 str;你写下 -> int,函数返回 str 也照跑不误。注解只是「写给静态检查器看的元数据」,解释器本身完全无视它。
下面这段代码在 3.14.6 上原样运行,没有任何报错:
def add(a: int, b: int) -> int:
return a + b
print(add(1, 2)) # 正常
print(add("x", "y")) # 传 str 也能跑
print(add([1], [2])) # 传 list 也能跑
真实输出:
3
xy
[1, 2]
add("x", "y") 之所以能跑,是因为 + 对字符串就是拼接;add([1], [2]) 能跑,是因为 + 对列表就是连接。注解 int 在这里一点约束力都没有。记住:Python 的类型是「动态类型」的,注解只服务于工具。
9.1.2 注解的基本语法
变量、函数参数、返回值都可以加注解:
name: str = "Alice"
scores: dict[str, int] = {"math": 95}
flag: bool
def greet(who: str, times: int = 1) -> str:
return who * times
class Point:
x: int
y: int = 0
几个要点:
- 变量注解只是「声明」,
flag: bool不会给flag赋任何值,运行时它仍未定义。 - 类体里的
x: int只记录到Point.__annotations__,不会自动变成实例属性(第 6 章讲过 dataclass 会帮你补上)。 - 参数默认值写在
=之后,注解写在:之后,顺序是参数名: 类型 = 默认值。
9.1.3 从 typing 到内置泛型
老代码里常见 from typing import List, Dict,然后写 List[int]。从 Python 3.9 起,直接用内置类型即可,不需要再导入 typing:
| 旧写法(仍可用,但不必) | 推荐写法(3.9+) |
|---|---|
List[int] | list[int] |
Dict[str, int] | dict[str, int] |
Tuple[int, str] | tuple[int, str] |
Set[str] | set[str] |
def average(nums: list[float]) -> float:
return sum(nums) / len(nums)
index: dict[str, list[int]] = {"a": [1, 2]}
本书基线是 3.12,所以正文一律用内置泛型写法。你会在很多老项目里看到 typing.List,认识它即可,不必照抄。
9.1.4 Optional、Union 与 |
表示「可能是 A 也可能是 B」用联合类型。Optional[int] 等价于 int | None:
from typing import Optional, Union
def f1(x: Optional[int]) -> int:
return x or 0
def f2(x: int | None) -> int: # 3.10 起可用
return x or 0
def f3(x: int | str) -> str: # 多类型联合
return str(x)
print(f1(None), f2(5), f3(3))
输出:
0 5 3
X | Y 这种写法叫「联合类型运算符」,Python 3.10 起才可用。在更老的版本里只能写 Union[X, Y]。有一个高频误用:Optional[int] 不是「可以省略的参数」,而是「值可以是 int 或 None」。想表达「有默认值」应该用 times: int = 1,两者完全不同。
9.1.5 Any / object / Callable / Never / NoReturn
这几个类型很容易混,我们用一张表厘清:
| 类型 | 含义 | 检查器行为 |
|---|---|---|
Any | 放弃检查 | 任何操作都允许,且会「污染」调用方 |
object | 任何对象 | 允许,但只能调用所有对象都有的方法 |
Callable[[int], str] | 可调用对象 | 检查参数与返回类型 |
Never | 永远不会正常返回/没有值 | 表示不可能到达 |
NoReturn | 函数永不返回 | 与 Never 语义等价(但并非同一个对象) |
from typing import Any, Callable, Never, NoReturn
def apply(fn: Callable[[int], str], x: int) -> str:
return fn(x)
def crash() -> NoReturn:
raise RuntimeError("boom")
def impossible(x: Never) -> None: ...
a: Any = 1
a.whatever.you.want() # 检查器不会报错——Any 关掉了检查
o: object = "hello"
# o.upper() # 检查器会报错:object 没有 upper
print(apply(str, 42))
输出:
42
Any 与 object 的关键差别:Any 让检查器闭嘴,object 让检查器严格。当你懒得写类型时用 Any 很方便,但它会沿着调用链一路关闭检查,等于在代码里挖了个洞。object 更安全:它是所有类型的基类,但只暴露「所有对象都有的」接口。
9.1.6 Literal / Final / Annotated
三个常用的「增强注解」:
from typing import Literal, Final, Annotated
Mode = Literal["r", "w", "a"] # 只能是这三个字面量之一
MAX_RETRY: Final = 3 # 常量,不允许再赋值
Port = Annotated[int, "1-65535"] # 给类型挂一段元数据
def open_file(path: str, mode: Mode) -> None:
...
open_file("a.txt", "r")
# open_file("a.txt", "x") # 检查器报错:x 不在 Literal 里
Literal["r", "w"]把「取值集合」写进类型,非常适合枚举式的字符串参数。Final表示「这是个常量」;MAX_RETRY = 4之后再赋值,检查器会报错。Annotated[int, "1-65535"]在类型上附加元数据,Pydantic(9.3 节)和 FastAPI 大量用它。
9.1.7 TYPE_CHECKING 与循环导入
有时注解需要引用一个「会造成循环导入」的类型(回顾 循环导入、命名空间包与惰性导入
)。解决办法是把导入放进 TYPE_CHECKING 块——它在运行时为 False,只在静态检查时生效:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .models import Order # 只在检查时导入,运行时不执行
def process(order: Order) -> None:
...
配合 from __future__ import annotations,函数签名里的注解会变成字符串,运行时不再求值,于是根本不需要真的导入 Order。这是打破循环依赖的经典手法。
9.1.8 用 mypy 与 pyright 做静态检查
注解本身不检查,那谁来检查?答案是第三方静态类型检查器。主流有两个:
| 工具 | 语言 | 特点 |
|---|---|---|
mypy | Python | 老牌、可 --strict、生态成熟 |
pyright | TypeScript(Node) | 快、默认更严格、VS Code/Pylance 内核 |
安装(版本号来自 PyPI 实测):
pip install "mypy==2.4.0"
npm install -g "pyright@1.1.414"
拿一个真实文件 sample.py 来试:
def add(a: int, b: int) -> int:
return a + b
result: str = add(1, 2)
value = add("x", "y")
注意:这段代码依然能正常 python sample.py 运行——错误只有检查器看得见。先看 pyright:
sample.py:4:15 - error: Type "int" is not assignable to declared type "str"
"int" is not assignable to "str" (reportAssignmentType)
sample.py:5:13 - error: Argument of type "Literal['x']" cannot be assigned to parameter "a" of type "int" in function "add"
"Literal['x']" is not assignable to "int" (reportArgumentType)
sample.py:5:18 - error: Argument of type "Literal['y']" cannot be assigned to parameter "b" of type "int" in function "add"
"Literal['y']" is not assignable to "int" (reportArgumentType)
3 errors, 0 warnings, 0 informations
再看 mypy --strict:
sample.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "str") [assignment]
sample.py:5: error: Argument 1 to "add" has incompatible type "str"; expected "int" [arg-type]
sample.py:5: error: Argument 2 to "add" has incompatible type "str"; expected "int" [arg-type]
Found 3 errors in 1 file (checked 1 source file)
两者都精准地指出了三处问题。把代码改对之后,mypy 会告诉你:
Success: no issues found in 1 source file
分工建议:日常编辑用 pyright(配合编辑器即时反馈),CI 门禁用 mypy --strict(可写进脚本、结果稳定)。--strict 会打开一大批严格选项,是「新项目该有的默认」。
9.1.9 注解在运行时能被读到吗
能。函数的注解存在 __annotations__ 里,而 typing.get_type_hints() 会把它「求值」成真正的类型对象:
from typing import get_type_hints
def greet(name: str, times: int = 1) -> str:
return name * times
print(greet.__annotations__)
print(get_type_hints(greet))
输出:
{'name': <class 'str'>, 'times': <class 'int'>, 'return': <class 'str'>}
{'name': <class 'str'>, 'times': <class 'int'>, 'return': <class 'str'>}
这正是 Pydantic、FastAPI、dataclass 能在运行时「看懂」你类型的原因,9.3 节会展开。
9.1.10 3.14 起注解默认延迟求值(PEP 649/749)
Python 3.14 起,注解默认延迟求值(lazy evaluation):函数定义时不再立即求值注解表达式,而是等到真正被读取时才求值。这带来两个变化:
- 引用「尚未定义」的类型不再报错。下面这段在 3.14 上完全合法:
import annotationlib
def f(x: Later) -> None: # Later 此刻还没定义
...
class Later: ...
print(annotationlib.get_annotations(f, format=annotationlib.Format.VALUE))
{'x': <class '__main__.Later'>, 'return': None}
- 若在
Later定义之前就强制求值,则会抛NameError;用Format.FORWARDREF可拿到ForwardRef而不求值。annotationlib是 3.14 新增的标准库模块,专门用于读取注解。
在 3.14 之前,要获得同样的「前向引用」能力,只能靠 from __future__ import annotations 把注解变成字符串。3.14 起这一 future 导入已经变得多余,并进入废弃路线(PEP 649/749);新代码不需要再写它。跨版本库为了兼容 3.12/3.13,仍可能保留它,这属于正常现象。
小结
- 类型注解在运行时不做任何检查:
int传str也照跑,注解只服务静态检查器。 - 3.9 起用内置泛型
list[int]/dict[str, int],3.10 起可用X | Y,不必再写typing.List。 Any关闭检查、object保持严格;Literal/Final/Annotated分别表达字面量集合、常量与附加元数据。TYPE_CHECKING+from __future__ import annotations是打破循环导入的经典手法(回顾 5.3 节)。pyright(快、编辑器友好)与mypy --strict(CI 门禁)是两大主力检查器,能发现运行时发现不了的错误。- 注解在运行时可通过
__annotations__与typing.get_type_hints()读取;3.14 起注解默认延迟求值,由annotationlib支持。
下一节我们进入类型系统的进阶部分:泛型、Protocol、TypedDict,以及 3.12 引入的 PEP 695 新语法——它们决定了你能否把注解用在真正复杂的场景里。
阅读导航:上一节:装饰器原理与实战 · 下一节:泛型、Protocol、TypedDict 与 PEP 695 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。