很多团队的类型标注停留在"IDE 补全"用途。这篇把 3.12–3.14 的新类型工具按"解决什么问题"组织起来,每段代码都给出实测结论(CPython 3.13.15 / 3.14.0 实际运行;类型检查行为以 pyright/mypy 官方语义为准)。
一、泛型语法:2013 年的写法可以退休了
PEP 695:类型参数直接声明在类/函数上
# 旧写法(仍然有效,但过时)
from typing import TypeVar, Generic
T = TypeVar("T")
class Box(Generic[T]): ...
# 新写法(3.12+)
class Box[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None: ...
def pop(self) -> T: ...
新语法还把类型参数暴露为运行时属性,自省更直接(实测):
s = Box[int]()
print(type(s).__type_params__) # (T,) —— 实测
PEP 696:TypeVar 默认值(3.13+)
"泛型参数不填时默认是什么"终于可以声明了:
from typing import TypeVar
T = TypeVar("T", default=int)
class Client[Backend = HTTPBackend]: # PEP 695 语法 + 默认
...
实测要点:T.__default__ 与 T.has_default() 可编程访问;顺序规则为"无默认值在前、有默认值在后",反序直接 TypeError(3.13.15 实测复现)。适合 SDK 场景:Client[Backend = HTTPBackend]、Result[T, E = str]。
二、函数重载与守卫:让检查器"懂"你的分支
TypeIs(3.13+):双向收窄
TypeGuard 只收窄 True 分支;TypeIs 两边都收窄,语义贴近 isinstance。判别/校验函数请默认用它:
from typing import TypeIs
def is_str_list(v: list[object]) -> TypeIs[list[str]]:
return all(isinstance(x, str) for x in v)
运行时无魔法,纯给检查器看(实测运行正确)。
overload:比 Union 诚实的签名
from typing import overload
@overload
def get(key: str, default: None) -> str | None: ...
@overload
def get(key: str, default: str) -> str: ...
def get(key: str, default: str | None = None) -> str | None:
return _store.get(key, default)
调用方 get("a", "b") 会精确得到 str 而不是 str | None。overload 不生成运行时逻辑,只在检查期生效。
三、结构化字典:TypedDict 的全套玩法
from typing import TypedDict, NotRequired, ReadOnly
class Config(TypedDict):
name: ReadOnly[str] # 3.13 转正:契约键不可写
retries: int
timeout: NotRequired[float] # 3.11:该键可不出现
实测 c["name"] 正常读取;类型检查器拒绝 c["name"] = "x"。三件套组合起来可以精确描述"哪些字段是契约、哪些可选、哪些可写"。
再看 sentinel 默认值的类型表达:想区分"没传 default"与"传了 None",靠运行时默认值会让类型检查器丢失信息,正确姿势是 overload:
from typing import overload
_UNSET = object() # 模块级唯一哨兵
@overload
def get(key: str) -> str | None: ...
@overload
def get(key: str, default: str | None) -> str | None: ...
def get(key: str, default: object = _UNSET) -> str | None:
if default is _UNSET:
return _store.get(key)
return _store.get(key, default) # type: ignore[arg-type] # 运行时分支,检查期由 overload 接管
写库时把"魔法默认值"(sentinel)的语义用 overload 声明给检查器,比靠文档可靠。
四、dataclass 一家:从数据类到冻结继承
slots + kw_only + frozen 组合
from dataclasses import dataclass
@dataclass(slots=True, kw_only=True, frozen=True)
class Invoice:
id: str
amount: float
slots=True:省内存、防 typo 属性;kw_only=True:字段全部必须关键字传参,字段增删不破坏调用方;frozen=True:不可变,可哈希。
配合 3.13 的通用 copy.replace(invoice, amount=99.0),"不可变对象 + 定向修改副本"的模式完全成型。
继承默认值的坑(kw_only= 治不了的场景)
frozen 基类与可变子类混搭时,默认值继承顺序会导致 TypeError——用 field(kw_only=True) 修饰带默认值的字段,或让基类无默认值。这类问题在 3.13 的报错信息里已经能直接读到修复建议。
五、自定义泛型实战:一个类型安全的 Repository
把上面的东西拼起来——PEP 695 + 696 的组合:上界与默认值写在一行:
class Entity:
id: str
class Repository[T: Entity = Entity]:
def __init__(self) -> None:
self._items: dict[str, T] = {}
def put(self, e: T) -> None: self._items[e.id] = e
def get(self, id_: str) -> T: return self._items[id_]
PEP 695 语法下 class Repository[T: Entity = Entity] 一行同时声明了"上界 Entity"与"默认 Entity",不需要 Generic 基类。这是我们实测过语法合法性的结构(Cell[T = object] 在 3.13.15 上运行通过)。
六、运行时注解读取:3.14 的 annotationlib 与迁移指南
3.14 起注解惰性求值(PEP 649/749)。对使用注解的库作者影响最大:
变化点
- 裸前向引用不再需要引号(3.13/3.14 对照实测):
class Node:
def link(self, other: Node) -> Node: # 不加引号
return other
同一个文件,3.14 上定义与注解读取都正常({'other': Node, 'return': Node});3.13 上定义即抛 NameError: name 'Node' is not defined。3.13 及以前必须写 "Node" 或挂 from __future__ import annotations。
- 定义带注解的函数/类几乎零开销(官方文档;注解存进专用 annotate 函数,访问时才求值);
- 注解库读取:官方建议工具作者改用
annotationlib.get_annotations(功能覆盖并取代typing.get_type_hints的场景);
三种格式与精确语义
import annotationlib
def with_string() -> "Missing": # 字符串字面量:定义期不求值
return 1
实测:Format.STRING 返回 {'return': 'Missing'},Format.VALUE 默认同样返回 {'return': 'Missing'}(字符串字面量保留原样,不报错)。
裸名字注解的语义(3.14 实测):def g(arg: Missing)(Missing 未定义)在定义时不报错;读注解时 VALUE 抛 NameError、FORWARDREF 返回 ForwardRef 占位、STRING 返回源码文本——三种格式覆盖"严格求值 / 宽容占位 / 纯文本自省"三种工具需求。序列化、DI、校验框架请按需选格式,并在文档里写清楚你的库用哪种。
兼容层建议
支持 3.13 及以下的库:
try:
from annotationlib import get_annotations
except ImportError:
from typing import get_type_hints as get_annotations
并在 CI 里对 3.13/3.14 双跑测试——注解求值时机变了,__annotations__ 的触发副作用也变了。
七、typing_EXTENSIONS:提前用明天的 typing
本文提到的库级类型特性大多有 typing_extensions 后移植(TypeIs、ReadOnly、TypeVar(default=)、@deprecated 等);但 PEP 695 这类语法级特性无法用库后移植,只能靠升级解释器。策略:库代码对可后移植的特性直接 import typing_extensions 以支持老解释器;应用代码在确认最低版本后改用标准库。
八、类型检查的工程化清单
- pyright(VSCode 体验最好)与 mypy(插件生态最全)都完全支持上述特性;
- CI 建议
pyright --level error+-W error::DeprecationWarning组合; - 渐进式上标注:先
pyright --createstub生成公共接口存根,再逐模块补全; reveal_type()是最好的调试工具(typing.reveal_type,3.11 起无需 import 也可见)。
验证环境:Windows 11 x64;CPython 3.13.15 与 3.14.0(python-build-standalone,uv 安装)。所有运行时示例于 2026-09-28 实际执行,输出为真实输出;纯类型检查器行为(TypeIs/ReadOnly/overload/NotRequired 的收窄与拒绝规则)未在本文环境安装 pyright/mypy 验证,语义以 pyright 与 mypy 官方文档为准——文中凡未给出实测输出的类型检查器行为均为官方语义转述,不冒充实测。引自官方文档的内容以 docs.python.org 为准。