Python 类型系统
Python 的类型系统经历了从”纯动态鸭子类型”到”渐进类型(Gradual Typing)“的演变。理解这条演变路径,才能知道 Python 的类型标注在什么场景下真正有用、什么场景下只是装饰。
从鸭子类型到渐进类型
鸭子类型:Python 的原生哲学
Python 的核心类型哲学是”如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子”。不检查类型,只检查能力。
def process(item):
item.save() # 只要 item 有 save() 方法就行优势:灵活、代码少、适合快速迭代。 代价:类型错误只在运行时暴露,大型项目中”这个函数到底期望什么”难以追溯。
渐进类型:PEP 484 的妥协
Python 3.5+ 引入 Type Hints(PEP 484),允许添加可选的类型注解:
def process(item: Savable) -> None:
item.save()关键设计决策:
- 可选:不影响运行时行为,纯注解
- 用于文档和静态检查:配合 mypy / pyright 使用
- 哲学:类型是”辅助工具”,不是”强制约束”
这与 TypeScript 的”编译期强制”和 Rust 的”编译器证明”形成鲜明对比。
typing 模块核心工具
基本类型注解
from typing import Optional, Union, Callable, Iterator
# 可选值
def greet(name: Optional[str] = None) -> str:
return f"Hello, {name or 'World'}"
# 联合类型
def parse(value: Union[str, int]) -> str:
return str(value)
# 3.10+ 简写
def parse(value: str | int) -> str:
return str(value)
# 函数类型
Handler = Callable[[str, int], bool]
def process(data: str, handler: Handler) -> bool:
return handler(data, 42)泛型
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
# 3.12+ PEP 695 新语法
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()Protocol(结构化子类型)
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
# Circle 没有显式继承 Drawable,但结构兼容
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # ✅ 通过 — 结构化子类型Python Protocol ≈ TypeScript interface:两者都是结构化类型——“形状对得上就行”。区别在于 Python 的 Protocol 检查是可选的(需要 mypy),TS 的 interface 检查是强制的(tsc)。
类型检查工具选型
| 工具 | 实现语言 | 速度 | 特点 | 推荐场景 |
|---|---|---|---|---|
| mypy | Python | 中 | 标准参考实现,最严格 | CI 基线检查 |
| pyright | TypeScript | 快 | VS Code 默认(Pylance),增量检查 | IDE 即时反馈 |
| pytype | Python | 慢 | Google 出品,自动推断注解 | 大型遗留代码库 |
| pyre | OCaml | 快 | Facebook 出品,增量检查 | 大型 monorepo |
2026 年推荐:CI 用 mypy 做硬基线,IDE 用 Pyright/Pylance 做即时反馈。两者可共存。
Pydantic:运行时类型系统
Python 的类型注解在运行时默认不生效。Pydantic 改变了这一点:
from pydantic import BaseModel, ValidationError
class User(BaseModel):
name: str
age: int
email: str
# 运行时校验 — 类型错误当场报错
try:
user = User(name="Alice", age="not a number", email="[email protected]")
except ValidationError as e:
print(e) # age: Input should be a valid integerPython → TS 的关键认知差距:你习惯 Pydantic 在运行时保留类型信息并做校验。TS 的类型在运行时完全消失——必须用 Zod/Valibot 做”类型擦除后的校验”。这是 Python → TS 最常见的认知落差。
Python 类型系统的边界
能做什么
- ✅ 为函数参数和返回值添加类型注解
- ✅ 泛型类和函数(TypeVar / PEP 695)
- ✅ Protocol 实现结构化子类型
- ✅ TypeGuard 实现类型收窄
- ✅ Literal 类型约束具体值
- ✅ TypedDict 为 dict 添加类型
不能做什么
- ❌ 不影响运行时行为(除非用 Pydantic)
- ❌ 不支持条件类型(
T extends U ? X : Y) - ❌ 不支持可辨识联合的模式匹配(需手动
isinstance) - ❌ 类型推断能力弱于 TS(需更多显式注解)
- ❌ 不支持联合类型的自动收窄(需 TypeGuard)
与 TypeScript/Rust 的对比
| 维度 | Python | TypeScript | Rust |
|---|---|---|---|
| 类型哲学 | 鸭子类型 + 渐进类型 | 结构化类型 | 标称类型 + ADT |
| 检查时机 | 运行时 / 可选静态 | 编译时强制 | 编译时强制 |
| 类型擦除 | 运行时忽略注解 | 编译后完全消失 | 编译后完全消失 |
| 运行时校验 | Pydantic 可选 | Zod/Valibot 可选 | 不需要(编译器保证) |
| 泛型 | TypeVar / PEP 695 | <T> 内联 | <T> + trait bound |
| Protocol/接口 | Protocol(结构化) | interface(结构化) | trait(标称) |
| 类型体操 | 低 | 高 | 中 |
关联
- meta: 类型系统 — 跨语言类型系统全景
- 范式: 泛型编程 — 泛型的跨语言对比
- Python: 语法基础 — 基本类型与对象模型
- Python: 面向对象 — 类型与 OOP 的关系
- TypeScript: 类型系统 — 结构化类型对比
- Python 专题 MOC
延伸阅读
- 官方: typing 模块文档、PEP 484、PEP 695
- 工具: mypy 文档、Pyright 文档、Pydantic 文档
- 学术: Pierce《Types and Programming Languages》(TAPL)、Siek & Taha《Gradual Typing for Functional Languages》
- 实践: Python 类型系统速查表