Python 类型注解升级指南:从 typing 到 PEP 695

# Python 类型注解那些年踩过的坑:从 typing 到 PEP 695

 

Python 的类型注解这些年变化挺大,从最早的 `typing.List[str]`,到 `list[str]`(PEP 585),再到 PEP 695 引入的全新 `type` 声明语法。我自己的代码库一直停留在 typing 风格,最近做了一次全面升级,把所有类型注解都换成了新的写法,顺便把一些常见坑梳理一下。

 

## 一、PEP 585:内置类型支持泛型

 

Python 3.9 开始,可以直接用 `list[T]`、`dict[K, V]`、`tuple[T, ...]` 这样的写法,不再需要从 typing 里导入。运行时效果等价,但写起来更直观。

 

```python

# 旧写法

from typing import List, Dict, Optional

 

def find_user(user_id: int) -> Optional[Dict[str, List[str]]]:

    ...

 

# 新写法(Python 3.9+)

def find_user(user_id: int) -> dict[str, list[str]] | None:

    ...

```

 

但有个细节要注意:`list[T]` 在运行时并不会真的检查 T 的类型,类型检查只发生在 mypy/pyright 这类静态分析器里。下面这段代码运行时是合法的:

 

```python

def foo(x: list[int]) -> int:

    return x[0]

 

# 运行时不会报错,mypy 会报

result = foo(["hello", "world"])

```

 

写代码的时候还是要靠纪律,不要以为加上了类型注解就万事大吉。

 

## 二、PEP 604:Union 用 | 代替 Union[]

 

`Union[X, Y]` 已经被 `X | Y` 完全取代,Optional[X] 也可以写成 `X | None`。但实际项目里我建议在统一一种风格之前先看一下依赖库的最低 Python 版本——如果还要支持 3.8,那只能用 Union[]。

 

```python

# Python 3.10+

def parse(s: str) -> int | float:

    try:

        return int(s)

    except ValueError:

        return float(s)

```

 

`isinstance` 检查也要注意:

 

```python

# 3.10+ 之前需要 typing.Union

isinstance(x, (int, float))  # 这个一直有效

```

 

## 三、PEP 695:type 关键字、TypeVar 语法糖

 

这是 Python 3.12 引入的最大变化。自定义泛型类不再需要 TypeVar:

 

```python

# 旧写法

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

 

# 新写法(Python 3.12+)

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

```

 

类型别名也有了原生语法:

 

```python

# 旧写法

from typing import TypeAlias

UserId: TypeAlias = int

Vector: TypeAlias = list[float]

 

# 新写法(Python 3.12+)

type UserId = int

type Vector = list[float]

```

 

函数里用 type 声明 TypeVar 也清爽了:

 

```python

# 旧写法

from typing import TypeVar

T = TypeVar('T')

U = TypeVar('U')

 

def zip_map(a: list[T], b: list[U], f: Callable[[T, U], T]) -> list[T]:

    return [f(x, y) for x, y in zip(a, b)]

 

# 新写法(Python 3.12+)

def zip_map[T, U](a: list[T], b: list[U], f: Callable[[T, U], T]) -> list[T]:

    return [f(x, y) for x, y in zip(a, b)]

```

 

## 四、踩过的几个具体坑

 

### 1. TypeVar 的 bound 和 constraints

 

bound 限制类型必须是某个类的子类,constraints 限制类型是几个具体类型之一。区别很容易搞混:

 

```python

from typing import TypeVar

 

# bound:必须是 int 的子类

T_bound = TypeVar('T_bound', bound=int)

 

# constraints:必须是这几个之一

T_constr = TypeVar('T_constr', int, str)

```

 

bound 允许传入 None(如果 bound 是 Optional),constraints 严格要求是某个具体类型。

 

### 2. 前向引用

 

类内引用自己需要用字符串:

 

```python

class Node:

    def __init__(self, next: 'Node | None' = None) -> None:

        self.next = next

 

# 或者用 from __future__ import annotations

from __future__ import annotations

 

class Node:

    def __init__(self, next: Node | None = None) -> None:

        self.next = next

```

 

PEP 563 的延迟求值在 Python 3.12 之后已经不再是默认行为了,from __future__ import annotations 显式启用更稳。

 

### 3. dataclass + Generic

 

泛型 dataclass 之前的写法很别扭:

 

```python

from dataclasses import dataclass

from typing import Generic, TypeVar

 

T = TypeVar('T')

 

@dataclass

class Box(Generic[T]):

    value: T

 

# PEP 695

@dataclass

class Box[T]:

    value: T

```

 

新版短了不少,可读性好太多。

 

### 4. 协变与逆变

 

协变(covariant)、逆变(contravariant)、不变(invariant)这几个概念在 Variance 出现之前只能靠 TypeVar 标记:

 

```python

from typing import TypeVar

 

# T_co 是协变的(只能作为返回值)

T_co = TypeVar('T_co', covariant=True)

 

# T_contra 是逆变的(只能作为参数)

T_contra = TypeVar('T_contra', contravariant=True)

 

# 3.12 之后可以在声明时直接标记

class Producer[T_co](Generic[T_co]):

    def produce(self) -> T_co: ...

 

class Consumer[T_contra](Generic[T_contra]):

    def consume(self, item: T_contra) -> None: ...

```

 

实际项目里用到的情况不算多,但写框架或者库的时候绕不开。

 

## 五、工具链配合

 

升级到 PEP 695 之后,mypy 和 pyright 都支持了,但版本要够新:

 

- mypy >= 1.7

- pyright >= 1.1.300

 

如果你的库还要发布到 PyPI,要在 setup.cfg/pyproject.toml 里明确写出 Python 版本约束:

 

```toml

[project]

requires-python = ">=3.12"

```

 

不然用户装到老版本 Python 上 import 直接报错。

 

## 总结

 

类型注解这个东西,写的时候费点时间,但维护期收益巨大。尤其是大项目,没有类型注解的代码改起来心惊胆战。

 

PEP 695 之后 Python 的类型系统终于像现代语言了,可读性和表达力都上了一个台阶。还在用 TypeVar 老写法的项目,强烈建议升级一次,体验完全不一样。

 

唯一要注意的是版本兼容——如果你的库要支持 3.11 及以下,就只能继续用 typing.TypeVar。但内部项目、3.12+ 的服务,能用新语法就用新语法,写起来真的爽。

 

posted @ 2026-06-05 09:14  fitch_liu  阅读(5)  评论(0)    收藏  举报