关于 python-typing
面向 Python 3.12+ 项目的类型标注规范指南,供开发者和编码 Agent 参考。涵盖现代语法、类型检查及数据结构选择,强调明确表达类型并避免滥用 Any。
主要功能
- 01为公开签名添加类型标注
- 02采用现代 Python 类型语法
- 03以精确类型替代 Any
- 04使用泛型与 Protocol
- 05根据数据选择结构
- 06配置严格类型检查
适用场景
- 01统一项目的 Python 类型标注规范
- 02为新函数和类设计类型签名
- 03排查宽泛的 Any 与类型忽略
- 04在 CI 中落实严格类型检查
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ aymericderbois/ skills --skill python-typing安装后在 Claude Code 中输入/python-typing确认已加载。
详细介绍
python-typing
变量、参数和返回值的类型标注至关重要。它可以:
- 在运行前发现 bug
- 为代码编写文档
- 帮助 LLM 理解代码
- 在 IDE 中提供更好的自动补全
目标:Python 3.12+
类型检查器(根据项目配置和已安装的工具选择):
| 工具 | + | − |
|---|---|---|
pyrefly (Meta, Rust) | 非常快速、现代 | 较新,生态系统仍在发展中 |
ty (Astral, Rust) | 非常快速,集成于 uv/ruff | 仍处于预览阶段 |
mypy --strict | 成熟、广泛使用、行业标准 | 较慢 |
pyright (Microsoft) | 快速,narrowing 能力出色 (Pylance) | 依赖 Node |
词汇表
以下技术术语统一采用这些含义:
- 类型标注 — 写在代码中的类型(
age: int、-> User)——官方术语(PEP 526)。 - 类型检查器 — 检查类型标注是否一致的工具(pyrefly、mypy,……)。
- 类型推断 — 类型检查器无需显式标注而自行推导出的类型。
- Generic — 一个传递输入类型至输出的类型参数(
T)。 Protocol— 由其形式(方法)定义的类型,无需继承:经过验证的 鸭子类型。- 鸭子类型 — 根据对象能做什么来接受它,而不是根据它所属的类。
Any— 会在其出现的位置禁用类型检查的类型。cast— 用于向type-checker指定类型。
规则
1. 为所有公开签名添加类型标注
- 必须为所有函数、方法和类属性添加类型标注:参数、返回值和属性。
- 必要时,在函数或方法内部为代码添加类型标注,以提高可读性(例如,为不够明确的函数返回值添加文档说明)。
def get_user(user_id: str) -> User | None:
"""Renvoie l'utilisateur s'il existe, None sinon."""
...
def do_something(v: str) -> None:
"""
Le type de retour de process_value() n'est pas explicite, le nom de la fonction ne donne pas
d'information claire, on peut donc typer le résultat pour améliorer la lisibilité.
"""
result: int = process_value(v)
...
class UserRepository:
user: User | None
def __init__(self, db: Database) -> None: # -> None explicite sur __init__
self._db = db
async def save(self, user: User) -> User: ...
2. 优先使用现代语法,而不是旧版 typing 别名
必须优先使用现代语法,而不是旧版 typing 别名(Optional、Union、List……)。
只有为兼容旧版 Python,才应使用旧版语法。
| 避免使用 | 改为 |
|---|---|
Optional[X] | X | None |
Union[A, B] | A | B |
List[X], Dict[K, V] | list[X], dict[K, V] |
Tuple[int, ...], Set[X] | tuple[int, ...], set[X] |
from typing import List, Dict | (无需导入) |
3. 避免使用 Any,优先选择精确类型
Any 会在其出现的位置禁用类型检查。
在可能的情况下,将 Any 替换为更精确的类型:
- 确实未知的数据 →
object:合法,但使用前必须先收窄类型。 - 可复用的容器或函数 → 使用 generic(规则 5),以保留类型。
- 未标注类型的库的输出 → 在入口处使用
cast(TypeVoulu, valeur),然后在后续代码中使用明确的类型。 - “具有某个方法的对象” → 使用
Protocol(规则 6)。
cast() 只声明类型,不会进行检查:仅用于确实已知类型的值,绝不能用来压制合理的错误——遇到这种情况,应修正实际类型。
如果必须消除某个特定错误,应明确指定代码:# type: ignore[return-value],绝不要使用裸的 # type: ignore,以免同时隐藏该行未来出现的错误。
4. 明确表示缺失
缺失是类型的一部分:使用 X | None 标注,而不是使用隐式哨兵值或未注明类型的 None。这样,类型检查器会要求先缩窄类型(is None、assert),再使用该值。
def find(user_id: str) -> User | None: # l'absence est dans la signature
...
u = find("42")
u.name # rejeté : u peut être None
if u is not None:
u.name # ok : restreint à User
5. 泛型:保留类型信息
泛型可以实现适用于多种不同类型的通用类型标注。采用 PEP 695 语法(3.12+)。 通常用于将函数/方法的输入类型传递到返回类型。
def first[T](items: list[T]) -> T:
# first([1, 2]) -> int
# first(["a"]) -> str
return items[0]
class Box[T]:
def __init__(self, value: T) -> None:
self._value = value
def get(self) -> T:
return self._value
6. Protocol:采用结构化类型,而非继承
Protocol 描述预期的形态(其方法),而不强制要求继承。
当你不拥有具体类,或只关心某个方法时,优先使用它。
from typing import Protocol
class Readable(Protocol):
def read(self) -> bytes: ...
def consume(source: Readable) -> bytes: # accepte fichier, socket, BytesIO… sans lien d'héritage
return source.read()
只有在你拥有整个类层次结构且希望共享实现时,才使用 ABC(显式继承)。
7. 选择合适的数据结构
根据数据的性质选择容器,而不是堆叠使用 dict[str, Any]:
TypedDict—— 键固定且已知的字典(JSON 负载、API 响应)。dataclass—— 带默认值、行为或不变量的对象。NamedTuple—— 不可变且轻量的命名元组。Literal/Enum—— 值的封闭集合(Literal["asc", "desc"]):类型检查器会拒绝集合之外的值,而str不会。
8. 命名并锁定
- 为复杂且反复使用的类型创建别名:
type Json = dict[str, "Json"] | list["Json"] | str | int | float | bool | None(PEP 695,3.12+)。有意义的名称胜过在各处重复联合类型。 - 使用
Final表示不可重新赋值的常量。 - 使用
ClassVar表示类属性(与实例属性相对)。 - 使用
Self(3.11+)表示返回自身类型的方法(-> Self),包括子类。 - 对预期重写其他方法的方法使用
@override(3.12+,或typing_extensions):如果父类重命名或删除了该方法,CI 就会失败。
警示信号——停止
- 正准备写
Any→ 能否改用object、泛型、Protocol或有针对性的cast? - 使用
# type: ignore却未指定代码 → 添加[code],否则会掩盖未来的错误。 - 从
typing导入Optional/List/Dict→ 改用现代写法(X | None、list、dict)。 - 用
cast()消除一个确实正确的错误 → 修正类型,而不是类型检查器。 - 注解写得很仔细,但 CI 中没有严格的类型检查器 → 类型标注就没有约束力。