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)。


类型检查工具选型

工具实现语言速度特点推荐场景
mypyPython标准参考实现,最严格CI 基线检查
pyrightTypeScriptVS Code 默认(Pylance),增量检查IDE 即时反馈
pytypePythonGoogle 出品,自动推断注解大型遗留代码库
pyreOCamlFacebook 出品,增量检查大型 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 integer

Python → 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 的对比

维度PythonTypeScriptRust
类型哲学鸭子类型 + 渐进类型结构化类型标称类型 + ADT
检查时机运行时 / 可选静态编译时强制编译时强制
类型擦除运行时忽略注解编译后完全消失编译后完全消失
运行时校验Pydantic 可选Zod/Valibot 可选不需要(编译器保证)
泛型TypeVar / PEP 695<T> 内联<T> + trait bound
Protocol/接口Protocol(结构化)interface(结构化)trait(标称)
类型体操

关联


延伸阅读