我先说个真实感受:以前看到 Python 里的泛型,心里总觉得“能用,但不太优雅”。你要定义一个泛型函数,必须写 T = TypeVar("T"),然后函数里再带上 def first(seq: list[T]) -> T,看起来还行,但一旦需要绑定类型、约束泛型或者定义变长参数,代码马上变得啰嗦。
好在前段时间升级到 Python 3.12,发现之前很多“反人类”的类型语法都被换掉了。新出的 PEP 695 把泛型定义直接放进了函数和类的声明里,不用再单独声明 TypeVar,代码清晰了不止一个档次。这篇文章不扯别的,就用实际案例把 PEP 695 的核心变化掰开揉碎。
1. 为什么 3.12 之前的泛型写起来让人头疼
看这个老式写法,定义一个返回列表第一个元素的函数:
from typing import TypeVar
T = TypeVar("T")
def first_element(values: list[T]) -> T:
return values[0]
这还行。但如果你的泛型需要满足一个协议,比如有 .price 属性,那你得写:
class Product:
price: float
P = TypeVar("P", bound=Product)
def get_total(products: list[P]) -> float:
return sum(p.price for p in products)
除了多写一个 TypeVar 变量,还必须在类和函数之间传递,导致你经常要往上翻找。而且 TypeVar 的 bound、covariant 这些参数很容易被小白误用。
老版本里还有一种更让人困惑的写法:你要在多个函数里复用同一个类型变量,就得把它定义成模块级变量,否则会在类型提示里变红。
PEP 695 的初衷,就是让类型参数直接“内联”到函数签名或类名后,不再单独声明。这本质上是一种语法糖,但糖得很舒服。
2. 新的类型参数语法:用 [ ] 取代 TypeVar
PEP 695 允许你在函数名后面直接跟方括号,里面写类型参数的名字,就像这样:
def first_element[T](values: list[T]) -> T:
return values[0]
解释一下:def first_element[T](...) 等同于定义了一个 TypeVar 叫 T,然后范围就是这个函数体。你不需要再单独写 T = TypeVar("T"),类型检查器也会把 T 自动当作泛型变量。
对类来说也一样:
class Box[T]:
def __init__(self, item: T):
self.item = item
def get(self) -> T:
return self.item
实例化的时候直接使用具体类型:
box = Box[int](42) print(box.get()) # 42
这种写法显然更干净。最关键的是,函数和类的泛型现在可以定义在作用域内部,不会污染模块命名空间,也不怕不同模块里的同名 T 冲突了。
3. 处理多个类型参数的场景
以前多类型参数要定义好几个 TypeVar,现在直接在方括号里用逗号隔开:
def zip_map[K, V](keys: list[K], values: list[V]) -> dict[K, V]:
return dict(zip(keys, values))
你再也不用写下边这样啰嗦的代码:
K = TypeVar("K")
V = TypeVar("V")
def zip_map(keys: list[K], values: list[V]) -> dict[K, V]:
return dict(zip(keys, values))
真的,就一个语法变化,代码量减少一小半,心智负担也降低不少。
4. 类型参数的约束:用冒号代替 TypeVar bound
如果你希望泛型 T 必须是某个特定类的子类,新语法直接在方括号里写:
from decimal import Decimal
def total[P: Product](products: list[P]) -> float:
return sum(p.price for p in products)
等价于老写法:
P = TypeVar("P", bound=Product)
def total(products: list[P]) -> float:
return sum(p.price for p in products)
可以看到,新写法把 bound 信息直接放在了参数名后面,语义更加直观。
另外,你还可以用 tuple[T, ...] 这种新的语法表示变长元组,PEP 695 也支持了。比如:
def vector[T](*args: T) -> tuple[T, ...]:
return args
这在旧版里要这样做:
T = TypeVar("T")
def vector(*args: T) -> tuple[T, ...]:
return args
顺便提一嘴,新语法还支持 *Ts 这种可变类型参数(TypeVarTuple)的简写,但普通开发中基本用不到,这里先不展开。
5. 泛型类之间的继承
在继承时使用泛型,新语法也顺手了不少。以前定义一个非泛型的子类,你需要写上泛型参数,比如:
T = TypeVar("T")
class Base(Generic[T]):
def method(self) -> T: ...
class IntBox(Base[int]):
pass
现在呢?
class Base[T]:
def method(self) -> T: ...
class IntBox(Base[int]):
pass
基类的泛型参数直接写进方括号,不用再 import Generic 了。对使用者来说,都是一样的。
如果你希望子类也保留泛型,可以这样:
class Base[T]:
pass
class Sub[T](Base[T]):
pass
是不是有点类似 TypeScript 的那种泛型写法?确实,Python 这次借鉴了很多静态语言的风格,反而让代码更容易读了。
6. 泛型类型别名也变了
老版本中,定义一个类型别名常常要这样玩:
from typing import TypeAlias DictStrInt = dict[str, int]
PEP 695 虽然没改变类型别名的常规用法,但结合新的泛型语法,你可以这样定义泛型别名:
type Matrix[T] = list[list[T]]
然后直接使用:
def print_matrix(m: Matrix[int]) -> None:
for row in m:
print(row)
这里的 type 关键字是 Python 3.12 引入的,专门用来声明类型别名。相比之前用 TypeAlias 再配合赋值,现在看起来更像定义类型本身。
还有一个小细节:泛型别名的类型参数也可以有约束:
type Tagged[T: str] = tuple[T, int]
不过实际上 str 已经是不可变类型,这种约束意义不大,只是展示语法。
7. 实战用例:写一个简单的 Repository 基类
光看语法片段不过瘾,我拿平时做后端常用的 Repository 模式来举个例子。
假设我们有一个 ORM 模型基类,想要定义一个通用的仓储基类,里面用泛型约束模型类型:
from typing import Iterator
class Model:
id: int
class User(Model):
name: str
class Product(Model):
price: float
class Repository[M: Model]:
def __init__(self, data: dict[int, M]):
self._data = data
def get_by_id(self, model_id: int) -> M:
return self._data[model_id]
def list_all(self) -> Iterator[M]:
return iter(self._data.values())
这以前要写多少 TypeVar?现在直接泛型参数 M 继承自 Model,简洁明了。实际使用:
user_repo = Repository[int, User]? # 不对,我们只用了 M 一个参数,但 dict[int, M] 的类型是明确的
user_repo = Repository[User]({1: User(id=1, name="Tony")})
print(user_repo.get_by_id(1).name)
仔细看 class Repository[M: Model],就是定义了一个类型参数 M,并约束 M 必须是 Model 的子类。内部 dict[int, M] 表示 key 是 int 类型,value 是 M 类型。很棒。
8. 与 Pydantic、FastAPI 结合的感受
很多小伙伴日常用 FastAPI 做 API,里面大量使用类型提示。PEP 695 虽然目前主要靠静态扫描,但 Pydantic 和 FastAPI 已经在做适配。
比如你定义一个泛型返回值:
class ApiResponse[T]:
data: T
message: str
在 FastAPI 里返回这个类时,如果用了 3.12 的新语法,类型检查器能更准确地推断出 data 字段的实际类型。这在编译期就能发现 bug,而不是等到运行时。
不过要注意,目前 Pydantic v2 对 PEP 695 的支持还有些坑。你在写 class Box[T]: 时,Pydantic 可能不能自动解析内部的泛型模型。官方表示在推进,但目前建议使用 BaseModel 时还是用旧方式。如果你不是写库,只是业务代码,影响不大。
9. 几个容易踩的坑
新语法刚上手,总有地方会卡壳。我列几个自己遇到过的:
- 不要在类名后的方括号里用引号。老版本 TypeVar 有字符串前向引用,但新语法是真实的对象,不能写
class Foo["Bar"],需要直接写class Foo[Bar],如果 Bar 还没定义,可以用if TYPE_CHECKING规避。 - 类型参数的作用域仅在当前函数/类内部,如果在方法里需要使用外层的泛型,请直接用方法参数里的泛型类,比如
self的类型是Self,不容易混淆。 - 新语法目前需要 Python 3.12 以上解释器,并且 Pyright/Pycharm 最新版才支持。VSCode 的 Pylance 已经适配,旧版 PyCharm 可能显示为错误。
- 如果你的项目里大量使用 `from typing import TypeVar`, 升级后不删也没关系,新旧可以混用。但最好统一使用新语法,免得代码风格不一。
10. 总结:新语法到底值得换吗
在我看来,PEP 695 不是那种“必须要立刻用起来”的更新,但它确实让代码的“类型感”更强烈了。以前很多人不喜欢写类型提示,就是觉得 TypeVar 麻烦;现在语法简单了,大家自然更愿意写泛型。
当你写一个通用工具库的时候,这种改进尤其明显。它的意义不只是少写两行代码,而是让你思考泛型的方式更直接——类型就是参数的一部分,像函数参数一样放在那里,不用绕弯子。
但如果你还在用 3.10 或更低版本,也不要急着升,毕竟升级不是小事。先把新语法在单独的环境里跑一跑,感受一下,等项目慢慢迁移。未来三年内,3.12 会成为主流,那时候再换完全来得及。
反正我现在已经把所有新写的代码都用上了 PEP 695。Python 的类型系统在慢慢进化,这是好事。就像当年从 Python 2 到 Python 3 一样,虽然还有阵痛,但往前看,方向总是好的。

