Docling 仓库 Dignified Python 规范:Python 3.13 类型标注指南(PEP 649 与 PEP 695 实战)
本文基于 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.toml的requires-python、setup.py/setup.cfg的python_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)改变了注解的求值时机——注解在定义时不再立即求值,而是被推迟到实际访问时才解析。这带来了三个直接后果:
- 前向引用无需引号:类内部引用自身类型不再需要字符串形式;
- 循环导入不再引发注解错误:两个模块互相引用对方类型时,注解求值被推迟,避免了
ImportError; from __future__ import annotations应该移除:3.13 的原生延迟求值比它更好,保留它只会掩盖新行为、造成混乱。
同时,3.10–3.12 的所有类型特性继续可用。文档给出了完整的版本能力矩阵:
| 版本 | 新特性 |
|---|---|
| 3.13 | PEP 649 延迟注解求值;前向引用天然可用;循环导入不再报错;禁止使用 from __future__ import annotations |
| 3.12 | PEP 695 类型参数语法 def funcT -> T;type 语句定义类型别名 |
| 3.11 | Self 类型,用于自返回方法 |
| 3.10 | 内置泛型集合 list[T]、dict[K, V];`X |
哲学与一致性规则:类型标注服务于什么
文档把类型标注的价值归纳为三条主线,这也是后文所有具体规则的出发点:
代码清晰度:类型即内联文档,让函数契约显式化,降低读代码的认知负担,不追踪实现就能理解数据流。
IDE 支持:启用自动补全与智能建议、在运行前捕获拼写与属性错误、支撑重命名/移动/抽取等重构工具、支持跳转定义。
防错:静态分析阶段捕获类型不匹配;用显式的可选类型预防 None 相关错误;不用运行代码就能确认期望的输入输出;尽早发现 API 契约违反。
在此之上,文档规定了分级的一致性规则(🔴 MUST / 🟡 SHOULD / 🟢 MAY):
公共 API(强制级):
- 🔴 必须标注所有函数参数(
self和cls除外); - 🔴 必须标注所有函数返回值;
- 🔴 必须标注所有类属性(含私有属性);
- 🟡 应当标注模块级常量。
内部代码(灵活级):
- 🟡 在有助于清晰度时标注函数签名;
- 🟢 对类型不那么显然的复杂局部变量可以标注;
- 🟢 显而易见的情况(如
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.py、md_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 = 0、name = "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 时,指南给出明确的迁移清单:
- 移除
from __future__ import annotations——不再需要; - 考虑升级到 PEP 695 语法——更干净的泛型;
- 类型别名改用
type语句——比赋值更显式; - 移除带引号的前向引用——现在天然可用。
对照示例一目了然:
# 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——避免循环依赖的条件导入。
永远不需要:
List、Dict、Set、Tuple——用内置类型;Union——用|运算符;Optional——用X | None;Generic——用 PEP 695 类语法。
小结:这份指南的本质是把 3.13 的能力矩阵翻译成可执行的编码纪律——3.13 层(PEP 649)解决"引用何时求值"的问题,3.12 层(PEP 695、type)解决"泛型与别名怎么写得干净"的问题,3.11 层(Self)解决"自返回如何精确标注"的问题;而一致性规则、反模式与 ty 检查配置则保证了这套写法在团队协作中可落地、可验证。参照 Docling 仓库的做法,落地前先确认项目的最低 Python 版本,再按版本矩阵选择可用的特性,是避免兼容性与风格冲突的关键一步。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00