前言题面里「最新整理」四个字是关键——如果只是把变量和基础类型再讲一遍那和十年前的教程没有区别。真正需要「最新」的是类型注解这套机制从 3.5 引入typing到 3.9 支持内置泛型3.10 支持X | Y联合写法3.12 引入type语句和类型参数语法再到3.14 把注解的延迟求值变成默认行为PEP 649 / PEP 749这条线一直在变。用旧写法写新代码轻则被类型检查器抱怨重则踩到求值时机的坑。最普遍的误解只有一个但杀伤力很大以为加了类型注解Python 就会在运行时帮你检查类型。不会。注解默认只是「给工具看的元数据」运行时它既不校验也不强制转换。传错类型的实参程序照跑不误除非你额外用了某个会在运行时读注解并校验的库。本文讲三件事注解到底是什么、现代写法该怎么写、以及 3.14 的延迟求值改变了什么。示例会明确标注每个特性所需的 Python 版本并给出旧版本的替代写法。一、注解是什么不是什么注解annotation是写在冒号后面的表达式可以作用在变量、函数参数、函数返回值、类属性上。它不是运行时的类型检查、类型转换、接口约束。它是给类型检查器mypy、pyright 之类、IDE、以及文档工具消费的结构化元数据。# 适用于 Python 3.8def greet(name: str, times: int 1) - str:return (你好 name ) * timesprint(greet(小明, 2)) # 你好小明你好小明print(greet(123)) # 不报错注解不拦截错误类型最后一行能跑通正是「注解不检查」的证据。真正想做运行时校验得自己写判断或者用第三方库——但那已经超出标准库范围了。注解能被程序读到函数的注解放在__annotations__里模块级变量的注解放在模块的__annotations__里。二、typing 模块复杂类型怎么标注基础类型直接用内置名字就行int、str、float、bool、bytes、list、dict。但「列表里装的是字符串」这种信息光写list表达不出来需要泛型写法。历史上要用typing模块里的别名场景老写法typing现代写法现代写法所需版本字符串列表List[str]list[str]3.9字典Dict[str, int]dict[str, int]3.9元组定长Tuple[int, str]tuple[int, str]3.9集合Set[int]set[int]3.9任意类型AnyAny仍需 typing3.5可为 NoneOptional[int]int \None联合类型Union[int, str]int \str可调用Callable[[int], str]collections.abc.Callable[[int], str]3.9Optional[int]只是Union[int, None]的简写不代表参数可以省略——「可以省略」要靠默认值表达。Any表示「放弃检查」不是「任意类型都安全」标了Any就等于放弃了这一处的类型保障能少用就少用。# 适用于 Python 3.10from typing import Anydef total_score(scores: dict[str, int]) - int:return sum(scores.values())def find_user(uid: int) - dict[str, Any] | None:data {1: {name: 小明}, 2: {name: 小红}}return data.get(uid)print(total_score({语文: 90, 数学: 95})) # 185print(find_user(1))print(find_user(99)) # None如果运行环境是 3.9 或更早dict[str, int]里只有最外层写法可换成Dict|也要换成Optional[...]。三、版本演进从 3.9 到 3.123.9内置容器类型支持下标可以写list[int]、dict[str, int]同版本还引入了字典合并运算符|PEP 584和str.removeprefix()/str.removesuffix()。3.10引入了「联合类型运算符」X | Y以及结构化模式匹配match/case。3.12引入了type语句来声明类型别名以及类型参数语法函数和类可以直接写def first[T](items: list[T]) - T:PEP 695。同时typing.TypeAlias被标记为废弃官方建议改用type语句并计划在 3.18 移除。3.14注解的求值时机发生根本变化见下一节。# 适用于 Python 3.12# type 语句声明类型别名3.12 起type Vector list[float]# 类型参数语法3.12 起PEP 695def first[T](items: list[T]) - T:return items[0]v: Vector [1.0, 2.0]print(first([10, 20, 30])) # 10print(first(v)) # 1.0在 3.12 之前要表达同样的意思得这样写# 适用于 Python 3.8from typing import TypeVar, ListT TypeVar(T)def first(items: List[T]) - T:return items[0]Vector List[float]print(first([10, 20, 30]))四、PEP 649 / 7493.14 的延迟求值这是本节标题里「最新」二字真正的落点。在 3.14 之前函数、类、模块上的注解会在定义那一刻立即求值。这带来一个麻烦如果注解里引用了还没定义的名字比如类里引用自身就会在定义时抛NameError。过去的对策是把注解写成字符串或者加一句from __future__ import annotationsPEP 563让所有注解都变成字符串存着、永不求值。Python 3.14 起注解不再在定义时立即求值而是延迟到真正需要读取时才求值PEP 649配套的落地实现是 PEP 749。好处有两方面定义大量带注解的函数和类时少了立即求值的开销同时不需要再给前向引用套引号也不必为了这个去开from __future__ import annotations。需要读取注解时标准库提供了annotationlib模块可以指定按哪种格式取格式含义Format.VALUE求值成真实的运行时对象最接近 3.14 之前的行为Format.FORWARDREF尽量求值求不出来的部分用前向引用占位不抛异常Format.STRING返回字符串形式等价于from __future__ import annotations的效果# 适用于 Python 3.14from annotationlib import get_annotations, Formatdef f(x: Undefined, y: int) - None: # Undefined 从未定义也不报错passprint(get_annotations(f, formatFormat.STRING)) # 全部以字符串形式给出print(get_annotations(f, formatFormat.FORWARDREF)) # 能求的求真不能的占位上面这段在 3.14 之前会在def那一行就抛NameError。如果运行环境低于 3.14要在定义前把Undefined定义出来或改用字符串注解、加from __future__ import annotations。再看前向引用3.14 之前类里引用自身的方法返回值常常得写成字符串。# 适用于 Python 3.8class Node:def __init__(self, value: int, nxt: Node None) - None:self.value valueself.nxt nxtdef __repr__(self) - str:return fNode({self.value})print(Node(1, Node(2)))在 3.14 及以上Node的引号可以去掉直接写nxt: Node None也不会报错。但为了兼容更老的版本保留引号仍然是最保险的写法——引号在任何 3.x 下都合法。常见坑点以为注解会在运行时拦截错误类型。❌ 依赖def f(x: int) - int来保证调用方一定传整数。✅ 注解只给静态检查器和 IDE 用要运行时校验必须自己写判断或使用会读注解并校验的第三方库。在 3.9 之前的版本写list[int]。❌ 在 3.8 上执行def f(x: list[int]) - None:运行到这一行会抛TypeError。✅ 3.9 以下用from typing import List写List[int]或加from __future__ import annotations让注解不求值。把Optional[X]当成「可以省略这个参数」。❌ 写def f(x: Optional[int])却希望调用方能不传x。✅Optional[int]只表示「值可能是None」「可以不传」必须靠默认值x: int | None None。注解里引用了当前作用域没有的名字。❌def f(x: Undefined) - None:在 3.14 之前会在定义时就抛NameError。✅ 3.14 起延迟求值不会立刻报错更早的版本加from __future__ import annotations或用字符串注解。用了已废弃的类型别名写法。❌ 3.12 之后继续用typing.TypeAlias/typing.List/typing.Dict写新代码。✅ 类型别名改用type X ...语句容器泛型改用内置的list[...]、dict[...]。把Any当成万能安全阀。❌ 给所有拿不准的参数都标Any等于把类型检查关掉。✅ 尽量给出具体的联合类型Any只在确实无法表达时局部使用。以为__annotations__取到的永远是运行时对象。❌ 写了from __future__ import annotations后还指望f.__annotations__里的值是真实类型——它存的是字符串。✅ 需要真实类型时用typing.get_type_hints(f)求值或在 3.14 及以上用annotationlib指定格式。类型注解写对了就以为代码一定对。❌ 用类型检查器零报错作为「逻辑正确」的证明。✅ 类型检查只覆盖类型层面的错误逻辑错误、边界条件、并发问题它一概不管。总结特性引入版本说明类型注解3.0函数注解/ 3.5typing元数据不参与运行时校验from __future__ import annotations3.7PEP 563让注解以字符串形式保存内置泛型list[int]3.9替代typing.List联合写法 X \Y3.10type语句、类型参数语法3.12替代TypeAlias注解延迟求值默认开启3.14PEP 649 / 749定义时不再求值配annotationlib读取用新代码时记住三条容器泛型写内置形式、联合类型写|、别再从typing里搬那些已被内置类型取代的别名。3.14 的延迟求值让注解「什么时候算出来」这件事变得可控但「注解不参与运行时校验」这条铁律从 3.0 到现在都没变过。