首页
/ Docling 仓库 Dignified Python 规范:Python 3.13 类型标注指南(PEP 649 与 PEP 695 实战)

Docling 仓库 Dignified Python 规范:Python 3.13 类型标注指南(PEP 649 与 PEP 695 实战)

2026-09-06 12:05:09作者:邓越浪Henry

本文基于 Docling 仓库中 .agents/skills/dignified-python/versions/python-3.13.md 这份类型标注指南展开,系统讲解 Python 3.13 下类型标注的核心变化——PEP 649 延迟求值带来的前向引用自然化,以及 PEP 695 泛型语法、type 类型别名等 3.11/3.12 特性的完整用法。读完本文,你能掌握在 3.13 环境中编写、审查、重构类型标注的现代做法,并理解 Docling 这类支持多 Python 版本的真实代码库为何仍保留 from __future__ import annotations

指南定位:Docling 的 dignified-python 技能与版本探测机制

这份文档位于 Docling 仓库的 python-3.13.md,属于 dignified-python 技能(Agent 编码规范技能)下的版本特定参考文档。配套的 SKILL.md 定义了它的加载方式:

  • 触发条件:当用户询问 "make this pythonic"、"type hints / typing"、"code review" 等 Python 代码质量问题时自动加载;
  • 版本探测:依次检查 pyproject.tomlrequires-pythonsetup.py/setup.cfgpython_requires.python-version 文件,默认回退到 3.12;
  • 按版本加载:探测到 3.13 时加载 versions/python-3.13.md,本文即该文档的完整内容。

这里有一个值得注意的仓库事实:Docling 自身的 pyproject.toml 声明的是 requires-python = '>=3.10,<4.0',分类器覆盖 3.10 到 3.14。也就是说,Docling 是兼容 3.13 的,但最低版本是 3.10——这直接决定了它不能无条件采用 3.13 才有的 PEP 649 默认行为(详见后文"真实代码库中的对照"一节)。这份指南的价值在于:当你在一个最低版本就是 3.13 的项目中工作时,应该怎么写类型标注。

核心变化:PEP 649 让前向引用与循环导入自然成立

3.13 文档的开篇就点明了最关键的变化:

前向引用和循环导入天然可用,不再需要 from __future__ import annotations

PEP 649(Deferred Evaluation of Annotations)改变了注解的求值时机——注解在定义时不再立即求值,而是被推迟到实际访问时才解析。这带来了三个直接后果:

  1. 前向引用无需引号:类内部引用自身类型不再需要字符串形式;
  2. 循环导入不再引发注解错误:两个模块互相引用对方类型时,注解求值被推迟,避免了 ImportError
  3. from __future__ import annotations 应该移除:3.13 的原生延迟求值比它更好,保留它只会掩盖新行为、造成混乱。

同时,3.10–3.12 的所有类型特性继续可用。文档给出了完整的版本能力矩阵:

版本 新特性
3.13 PEP 649 延迟注解求值;前向引用天然可用;循环导入不再报错;禁止使用 from __future__ import annotations
3.12 PEP 695 类型参数语法 def funcT -> Ttype 语句定义类型别名
3.11 Self 类型,用于自返回方法
3.10 内置泛型集合 list[T]dict[K, V];`X

哲学与一致性规则:类型标注服务于什么

文档把类型标注的价值归纳为三条主线,这也是后文所有具体规则的出发点:

代码清晰度:类型即内联文档,让函数契约显式化,降低读代码的认知负担,不追踪实现就能理解数据流。

IDE 支持:启用自动补全与智能建议、在运行前捕获拼写与属性错误、支撑重命名/移动/抽取等重构工具、支持跳转定义。

防错:静态分析阶段捕获类型不匹配;用显式的可选类型预防 None 相关错误;不用运行代码就能确认期望的输入输出;尽早发现 API 契约违反。

在此之上,文档规定了分级的一致性规则(🔴 MUST / 🟡 SHOULD / 🟢 MAY):

公共 API(强制级):

  • 🔴 必须标注所有函数参数(selfcls 除外);
  • 🔴 必须标注所有函数返回值;
  • 🔴 必须标注所有类属性(含私有属性);
  • 🟡 应当标注模块级常量。

内部代码(灵活级):

  • 🟡 在有助于清晰度时标注函数签名;
  • 🟢 对类型不那么显然的复杂局部变量可以标注;
  • 🟢 显而易见的情况(如 count = 0)可以省略。

基础集合类型:一律使用内置泛型

推荐——使用内置泛型类型:

names: list[str] = []
mapping: dict[str, int] = {}
unique_ids: set[str] = set()
coordinates: tuple[int, int] = (0, 0)

错误——不要使用 typing 模块的旧等价物:

from typing import List, Dict, Set, Tuple  # Don't do this
names: List[str] = []

原因:内置类型更简洁、无需导入,且自 3.10 起就是现代 Python 标准。Docling 代码库同样遵循这一约定——例如 document_converter.py 中的签名直接使用 -> BackendOptions | None 这类内置泛型与 | 联合语法,而不是 Optional[BackendOptions]

联合类型与可选类型:|X | None

推荐——使用 | 运算符:

def process(value: str | int) -> str:
    return str(value)

def find_config(name: str) -> dict[str, str] | dict[str, int]:
    ...

# Multiple unions
def parse(input: str | int | float) -> str:
    return str(input)

错误——不要使用 typing.Union

from typing import Union
def process(value: Union[str, int]) -> str:  # Don't do this
    ...

推荐——可选类型使用 X | None

def find_user(id: str) -> User | None:
    """Returns user or None if not found."""
    if id in users:
        return users[id]
    return None

错误——不要使用 typing.Optional

from typing import Optional
def find_user(id: str) -> Optional[User]:  # Don't do this
    ...

可调用对象类型:使用 collections.abc.Callable

推荐——从 collections.abc 导入 Callable

from collections.abc import Callable

# Function that takes int, returns str
processor: Callable[[int], str] = str

# Function with no args, returns None
callback: Callable[[], None] = lambda: None

# Function with multiple args
validator: Callable[[str, int], bool] = lambda s, i: len(s) > i

接口设计:ABC 优先,Protocol 用于结构化类型

推荐——接口使用 ABC:

from abc import ABC, abstractmethod

class Repository(ABC):
    @abstractmethod
    def get(self, id: str) -> User | None:
        """Get user by ID."""

    @abstractmethod
    def save(self, user: User) -> None:
        """Save user."""

🟡 可用——Protocol 仅用于结构化类型(structural typing):

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

def render(obj: Drawable) -> None:
    obj.draw()

指南明确说明:dignified-python 偏好 ABC,因为它让继承关系与意图显式化。Docling 的抽象后端体系正是这种显式继承风格的实例——各类 backend 继承统一的抽象后端基类,而非依赖结构匹配。

Self 类型:自返回方法的正确标注(3.11+)

推荐——构造器等自返回方法使用 Self

from typing import Self

class Builder:
    def set_name(self, name: str) -> Self:
        self.name = name
        return self

    def set_value(self, value: int) -> Self:
        self.value = value
        return self

Self(PEP 673,3.11 引入)让类型检查器知道返回的是具体子类的实例而非基类实例,链式调用时能保留子类的全部方法签名。

PEP 695 泛型函数与类(3.12+)

推荐——泛型函数使用 PEP 695 类型参数语法:

def firstT -> T | None:
    """Return first item or None if empty."""
    if not items:
        return None
    return items[0]

def identityT -> T:
    """Return value unchanged."""
    return value

# Multiple type parameters
def zip_dictsK, V -> dict[K, V]:
    """Create dict from separate key and value lists."""
    return dict(zip(keys, values))

🟡 可用——TypeVar 仍然有效:

from typing import TypeVar

T = TypeVar("T")

def first(items: list[T]) -> T | None:
    if not items:
        return None
    return items[0]

注意:简单泛型优先用 PEP 695 语法;约束/边界场景仍需 TypeVar

推荐——泛型类使用 PEP 695 类语法:

class Stack[T]:
    """A generic stack data structure."""

    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> Self:
        self._items.append(item)
        return self

    def pop(self) -> T | None:
        if not self._items:
            return None
        return self._items.pop()

# Usage
int_stack = Stack[int]()
int_stack.push(42).push(43)

🟡 可用——TypeVar + Generic 旧式写法仍然有效:

from typing import Generic, TypeVar

T = TypeVar("T")

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
    # ... rest of implementation

PEP 695 的优势:无需导入,且类型参数的作用域局部于该类,不会污染模块命名空间。

类型参数边界与约束型 TypeVar(3.12+)

边界(bounds)用 PEP 695

class Comparable:
    def compare(self, other: object) -> int:
        ...

def max_valueT: Comparable -> T:
    """Get maximum value from comparable items."""
    return max(items, key=lambda x: x)

约束(constraints)仍用 TypeVar——这是 PEP 695 目前不支持的语法:

from typing import TypeVar

# Constrained to specific types - must use TypeVar
Numeric = TypeVar("Numeric", int, float)

def add(a: Numeric, b: Numeric) -> Numeric:
    return a + b

错误——PEP 695 方括号参数不会把类型约束到 int | float

# This doesn't constrain to int|float
def addNumeric -> Numeric:
    return a + b

这一点在 Docling 源码中得到印证:service_client/job.py 中仍然使用 T_Result = TypeVar("T_Result") 配合 Generic[T_Result](如 _JobHandlers_AsyncJobHandlers_ConversionJobBase)来表达泛型作业处理链。虽然这是简单参数(理论上可改写为 PEP 695),但这是最低版本 3.10 的项目,写法合规且与仓库约定一致——它恰好说明:PEP 695 并非强制迁移项,TypeVar 在约束、边界与兼容性场景下依然是标准工具

type 语句:更明确的类型别名(3.12+)

推荐——使用 type 语句:

# Simple alias
type UserId = str
type Config = dict[str, str | int | bool]

# Generic type alias
type Result[T] = tuple[T, str | None]

def process(value: str) -> Result[int]:
    try:
        return (int(value), None)
    except ValueError as e:
        return (0, str(e))

🟡 可用——普通赋值别名仍然有效:

UserId = str  # Still valid
Config = dict[str, str | int | bool]  # Still valid

type 语句更显式,且支持泛型别名Result[T] 这种带类型参数的别名只有 type 语句能做到)。

前向引用与循环导入:3.13 的新能力

这是 3.13 章节最核心的实操内容。

正确——PEP 649 下天然成立:

# Forward reference - no quotes needed!
class Node:
    def __init__(self, value: int, parent: Node | None = None):
        self.value = value
        self.parent = parent

# Circular imports - just works!
# a.py
from b import B

class A:
    def method(self) -> B:
        ...

# b.py
from a import A

class B:
    def method(self) -> A:
        ...

# Recursive types - no future needed!
type JsonValue = dict[str, JsonValue] | list[JsonValue] | str | int | float | bool | None

错误——3.13 中不要使用 from __future__ import annotations

from __future__ import annotations  # DON'T DO THIS in Python 3.13

class Node:
    def __init__(self, value: int, parent: Node | None = None):
        ...

为什么 3.13 要避开这个 future 导入:

  • 不必要——PEP 649 提供了更好的默认行为;
  • 容易造成混淆;
  • 它掩盖了 3.13 原生的延迟求值机制;
  • 让你无法充分利用这些改进。

真实代码库中的对照:Docling 的最低 Python 是 3.10(requires-python = '>=3.10,<4.0'),而在 3.13 之前,没有 future 导入时注解会在定义处立即求值,自引用与循环引用会直接抛错。因此 Docling 的众多模块(如 msword_backend.pymd_backend.py 等 20 余个 backend/datamodel 文件)保留了 from __future__ import annotations——这与 3.13 指南并不矛盾:指南约束的是"最低版本即 3.13"的项目,而 Docling 需要向后兼容到 3.10,只能依赖 future 导入实现等价的字符串化延迟求值。这提醒读者:判断该不该删掉 future 导入,先查项目的 requires-python

完整示例一:天然前向引用的树结构

from typing import Self
from collections.abc import Callable

class Node[T]:
    """Tree node - forward reference works naturally in 3.13!"""

    def __init__(
        self,
        value: T,
        parent: Node[T] | None = None,  # Forward ref, no quotes!
        children: list[Node[T]] | None = None,  # Forward ref, no quotes!
    ) -> None:
        self.value = value
        self.parent = parent
        self.children = children or []

    def add_child(self, child: Node[T]) -> Self:
        """Add child and return self for chaining."""
        self.children.append(child)
        child.parent = self
        return self

    def find(self, predicate: Callable[[T], bool]) -> Node[T] | None:
        """Find first node matching predicate."""
        if predicate(self.value):
            return self

        for child in self.children:
            result = child.find(predicate)
            if result:
                return result

        return None

# Usage - all type-safe with no __future__ import!
root = Nodeint
root.add_child(Nodeint).add_child(Nodeint)

注意这个示例同时集中了 3.11–3.13 的全部特性:PEP 695 泛型类(3.12)、Self 链式返回(3.11)、collections.abc.Callable,以及 PEP 649 免引号自引用(3.13)。

完整示例二:PEP 695 泛型仓储

from abc import ABC, abstractmethod
from typing import Self

class Entity[T]:
    """Base class for entities."""

    def __init__(self, id: T) -> None:
        self.id = id

class RepositoryT:
    """Generic repository interface."""

    @abstractmethod
    def get(self, id: str) -> T | None:
        """Get entity by ID."""

    @abstractmethod
    def save(self, entity: T) -> None:
        """Save entity."""

    @abstractmethod
    def delete(self, id: str) -> bool:
        """Delete entity, return True if deleted."""

class User(Entity[str]):
    def __init__(self, id: str, name: str) -> None:
        super().__init__(id)
        self.name = name

class UserRepository(Repository[User]):
    def __init__(self) -> None:
        self._users: dict[str, User] = {}

    def get(self, id: str) -> User | None:
        if id not in self._users:
            return None
        return self._users[id]

    def save(self, entity: User) -> None:
        self._users[entity.id] = entity

    def delete(self, id: str) -> bool:
        if id not in self._users:
            return False
        del self._users[id]
        return True

通用最佳实践:具体性、Union 节制、显式 None、慎用 Any

优先具体类型:

# ✅ GOOD - Specific
def get_config() -> dict[str, str | int]:
    ...

# ❌ WRONG - Too vague
def get_config() -> dict:
    ...

Union 要节制,仅在必要时使用:

# ✅ GOOD - Union only when necessary
def process(value: str | int) -> str:
    ...

# ❌ WRONG - Too permissive
def process(value: str | int | list | dict) -> str | None | list:
    ...

None 保持显式:

# ✅ GOOD - Explicit optional
def find_user(id: str) -> User | None:
    ...

# ❌ WRONG - Implicit None return
def find_user(id: str) -> User:
    return None  # Type checker error!

尽量避免 Any

# ✅ GOOD - Specific type
def serialize(obj: User | Config) -> str:
    ...

# ❌ WRONG - Defeats purpose of types
from typing import Any
def serialize(obj: Any) -> str:
    ...

什么时候该标、什么时候可以省

必须标注:

  • 公共函数签名(参数 + 返回值);
  • 类属性(包括私有属性);
  • 跨模块边界的函数参数;
  • 返回值不那么自明的场景。

有助于清晰度时标注:

  • 复杂局部变量;
  • 闭包与嵌套函数;
  • 作为回调使用的 lambda 表达式。

可以省略:

  • 显而易见的情况:count = 0name = "example"
  • 琐碎的私有辅助函数;
  • 类型标注不增加清晰度的测试 fixture 设置代码。

静态类型检查:ty 的使用与配置

指南指定 dignified-python 使用 ty 做静态类型检查:

# Check all files
ty check

# Check specific file
ty check src/mymodule.py

# Check with specific Python version
ty check --python-version 3.13

pyproject.toml 中的配置:

[tool.ty.environment]
python-version = "3.13"

--python-version 参数很关键:类型检查器按指定版本的语义解释代码,例如 PEP 649 的延迟求值与 PEP 695 语法只对 3.13/3.12 生效,配置错误的目标版本会导致误报或漏报。

反模式:三条红线

❌ 不要用 # type: ignore 掩盖类型错误——应该修复错误本身;确需桥接时使用 cast 显式声明意图:

# ❌ WRONG - Hiding type error
result = unsafe_function()  # type: ignore

# ✅ CORRECT - Fix the type error
result: Expected = cast(Expected, unsafe_function())

❌ 不要在类型标注中使用裸 Exception——异常应向外抛出并在文档中说明,而不是混进返回值联合类型:

# ❌ WRONG - No value from typing exception
def risky() -> str | Exception:
    ...

# ✅ CORRECT - Let exceptions bubble
def risky() -> str:
    ...  # Raises ValueError on error

❌ 不要给显而易见的简单场景过度标注:

# ❌ WRONG - Obvious from context
def add_numbers(a: int, b: int) -> int:
    result: int = a + b  # Unnecessary type annotation
    return result

# ✅ CORRECT - Type only signature
def add_numbers(a: int, b: int) -> int:
    result = a + b  # Type is obvious
    return result

从 3.10/3.11 迁移的四个步骤

从 3.10/3.11 迁移到 3.13 时,指南给出明确的迁移清单:

  1. 移除 from __future__ import annotations——不再需要;
  2. 考虑升级到 PEP 695 语法——更干净的泛型;
  3. 类型别名改用 type 语句——比赋值更显式;
  4. 移除带引号的前向引用——现在天然可用。

对照示例一目了然:

# Python 3.10/3.11
from __future__ import annotations
from typing import TypeVar, Generic

T = TypeVar("T")

class Node(Generic[T]):
    def __init__(self, value: T, parent: "Node[T] | None" = None):
        ...

# Python 3.13
from typing import Self

class Node[T]:
    def __init__(self, value: T, parent: Node[T] | None = None):
        ...

迁移的适用前提:项目的 requires-python 下限已提升到 3.13。像 Docling 这样下限在 3.10 的项目,第 1、4 步会破坏兼容性,第 2、3 步(PEP 695、type 语句)也要到 3.12+ 才能用,因此整份迁移清单只对新项目或已抬升最低版本的项目成立。

收尾:3.13 下 typing 模块导入的取舍

指南最后用一张"保留/清除"清单总结了 3.13 时代的 typing 依赖面:

很少需要:

  • TypeVar——仅限约束/边界型类型变量;
  • Any——类型真正未知时少量使用;
  • Protocol——结构化类型(优先 ABC);
  • TYPE_CHECKING——避免循环依赖的条件导入。

永远不需要:

  • ListDictSetTuple——用内置类型;
  • Union——用 | 运算符;
  • Optional——用 X | None
  • Generic——用 PEP 695 类语法。

小结:这份指南的本质是把 3.13 的能力矩阵翻译成可执行的编码纪律——3.13 层(PEP 649)解决"引用何时求值"的问题,3.12 层(PEP 695、type)解决"泛型与别名怎么写得干净"的问题,3.11 层(Self)解决"自返回如何精确标注"的问题;而一致性规则、反模式与 ty 检查配置则保证了这套写法在团队协作中可落地、可验证。参照 Docling 仓库的做法,落地前先确认项目的最低 Python 版本,再按版本矩阵选择可用的特性,是避免兼容性与风格冲突的关键一步。

登录后查看全文
热门项目推荐
相关项目推荐