《Python编程入门》9.1 类型注解语法与 pyright / mypy

类型注解是 Python 静态分析的地基,运行时却几乎不做任何检查。本节从「写了 int 传 str 也能跑」讲起,梳理 Optional、Union、内置泛型、Literal、Final、Annotated 等语法,厘清 Any 与 object 的边界,再用 pyright 与 mypy --strict 在真实文件上跑出检查结果,并说明注解在运行时如何被读取、3.14 起为何默认延迟求值。

本节目标:掌握类型注解的常用语法,理解「注解在运行时不做检查」这一核心事实,并学会用 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 做静态检查

注解本身不检查,那谁来检查?答案是第三方静态类型检查器。主流有两个:

工具语言特点
mypyPython老牌、可 --strict、生态成熟
pyrightTypeScript(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):函数定义时不再立即求值注解表达式,而是等到真正被读取时才求值。这带来两个变化:

  1. 引用「尚未定义」的类型不再报错。下面这段在 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}
  1. 若在 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 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时