add-or-fix-type-checking
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAdd Or Fix Type Checking
添加或修复类型检查
Input
输入
- : module or directory to type-check (if known).
<target> - Optional or CI output showing typing failures.
make typing
- :要进行类型检查的模块或目录(如果已知)。
<target> - 可选的输出或CI输出,显示类型检查失败信息。
make typing
Workflow
工作流程
-
Identify scope from the failing run:
- If you already have or CI output, extract the failing file/module paths.
make typing - If not, run:
bash
make typing - Choose the narrowest target that covers the failures.
- If you already have
-
Runfor the target to get a focused baseline:
ty checkbashty check --respect-ignore-files --exclude '**/*_pb*' <target> -
Triage errors by category before fixing anything:
- Wrong/missing type annotations on signatures
- Attribute access on union types (for example )
X | None - Functions returning broad unions (for example )
str | list | BatchEncoding - Mixin/protocol self-type issues
- Dynamic attributes on objects or modules
- Third-party stub gaps (missing kwargs, missing , etc.)
__version__
-
Apply fixes using this priority order (simplest first):a. Narrow unions with/
isinstance()/if x is None. This is the primary tool for resolving union-type errors.hasattr()narrows through all of these patterns, including the negative forms:typython# Narrow X | None — use `if ...: raise`, never `assert` if x is None: raise ValueError("x must not be None") x.method() # ty knows x is X here # Narrow str | UploadFile if isinstance(field, str): raise TypeError("Expected file upload, got string") await field.read() # ty knows field is UploadFile here # Narrow broad union parameters early in a function body # (common for methods accepting e.g. list | dict | BatchEncoding) if isinstance(encoded_inputs, (list, tuple)): raise TypeError("Expected a mapping, got sequence") encoded_inputs.keys() # ty sees only the dict/mapping types nowb. Use local variables to help ty track narrowing across closures. Whenisself.xand you need to pass it to nested functions or closures,X | Nonecannot track thattystays non-None. Copy to a local variable and narrow the local:self.xpythonmanager = self.batching_manager if manager is None: raise RuntimeError("Manager not initialized") # Use `manager` (not `self.batching_manager`) in nested functionsc. Split chained calls when the intermediate type is a broad union. Iffails becausefunc().method()returns a union, split it:func()python# BAD: ty can't narrow through chained calls result = func(return_dict=True).to(device)["input_ids"] # GOOD: split, narrow, then chain result = func(return_dict=True) if not hasattr(result, "to"): raise TypeError("Expected dict-like result") inputs = result.to(device)["input_ids"]d. Fix incorrect type hints at the source. If a parameter is typedbut can never beX | Nonewhen actually called, removeNonefrom the hint.Nonee. Annotate untyped attributes. Add type annotations to instance variables set inor elsewhere (for example__init__). Declare class-level attributes that are set dynamically later (for exampleself.foo: list[int] = [],_cache: Cache)._token_tensor: torch.Tensor | Nonef. Usefor methods with input-dependent return types. When a method returns different types based on the input type (e.g.@overloadwith str vs int keys), use__getitem__to declare each signature separately:@overloadpythonfrom typing import overload @overload def __getitem__(self, item: str) -> ValueType: ... @overload def __getitem__(self, item: int) -> EncodingType: ... @overload def __getitem__(self, item: slice) -> dict[str, ValueType]: ... def __getitem__(self, item: int | str | slice) -> ValueType | EncodingType | dict[str, ValueType]: ... # actual implementationThis eliminatescalls at usage sites by giving the checker precise return types for each call pattern.cast()g. Make container classes generic to propagate value types. When a class likeholds values whose type changes after transformation (e.g. lists → tensors afterUserDict), make the class generic so methods can return narrowed types:.to()pythonfrom typing import Generic, overload from typing_extensions import TypeVar _V = TypeVar("_V", default=Any) # default=Any keeps existing code working class MyDict(UserDict, Generic[_V]): @overload def __getitem__(self, item: str) -> _V: ... # ... def to(self, device) -> MyDict[torch.Tensor]: # after .to(), values are tensors ... return self # type: ignore[return-value]The(fromdefault=Any) means unparameterized usage liketyping_extensionsstaysMyDict()— no existing code needs to change. Only methods that narrow the value type (likeMyDict[Any]) declare a specific return type. This eliminates.to()at all call sites.cast()h. Usefor mixins. When a mixin accesses attributes from its host class, define a Protocol inself: "ProtocolType"and annotatesrc/transformers/_typing.pyon methods that need it. Apply this consistently to all methods in the mixin. Import underselfto avoid circular imports.TYPE_CHECKINGi. Usefunctions for dynamic module attributes (for exampleTypeGuard,torch.npu,torch.xpu). Instead oftorch.compilerorgetattr(torch, "npu"), define a type guard function inhasattr(torch, "npu") and torch.npu.is_available():src/transformers/_typing.pypythondef has_torch_npu(mod: ModuleType) -> TypeGuard[Any]: return hasattr(mod, "npu") and mod.npu.is_available()Then use it as a narrowing check:. After the guard,if has_torch_npu(torch): torch.npu.device_count()treats the module asty, allowing attribute access withoutAnyorgetattr(). See existing guards incast()for all device backends._typing.pyKey rules for type guards:- Use (not a Protocol) — this is the simplest form that works with
TypeGuard[Any]and avoids losing the original module's known attributes.ty - The guard function must be called directly in an condition for narrowing to work.
ifdoes NOT narrow throughtyconditions orand.if not guard: return - Import guards with (not via module attribute
from .._typing import has_torch_xxx) —_typing.has_torch_xxxonly resolvestyfrom direct imports.TypeGuard
j. Use/getattr()for dynamic model/config attributes. For runtime-injected fields (for example config/model flags), usesetattr()for reads andgetattr(obj, "field", default)for writes. Also usesetattr(obj, "field", value)for third-party packages missing type stubs (for examplegetattr()). Avoidgetattr(safetensors, "__version__", "unknown")style — use type guards instead (see above).getattr(torch, "npu")k. Useas a last resort beforecast(). Use when you've structurally validated the type but the checker can't see it: pattern-matched AST nodes, known-typed dict values, or validated API responses.# type: ignorepython# After structural validation confirms the type: stmt = cast(cst.Assign, node.body[0]) annotations = cast(list[Annotation], [])Do not usefor module attribute narrowing — use type guards. Do not usecast()whencast()or generics can solve it at the source.@overloadl. Useonly for third-party stub defects. This means cases where the third-party package's type stubs are wrong or incomplete and there is no way to narrow or cast around it. Examples:# type: ignore- A kwarg that exists at runtime but is missing from the stubs
- A method that exists but isn't declared in the stubs
Always add the specific error code: , not bare
# type: ignore[call-arg].# type: ignore
- Use
-
Things to never do:
- Never use for type narrowing. Asserts are stripped by
assertand must not be relied on for correctness. Usepython -Oinstead.if ...: raise - Never use as a first resort. Exhaust all approaches above first.
# type: ignore - Do not use to access dynamic device backends (
getattr(torch, "backend"),npu,xpu,hpu,musa,mlu,neuron) — use type guardscompiler - Do not use for module attribute narrowing — use type guards
cast() - Do not use when
cast()or generics can eliminate it at the source@overload - Do not add helper methods or abstractions just to satisfy the type checker (especially for only 1-2 occurrences)
- Do not pollute base classes with domain-specific fields; use Protocols
- Do not add guards for values guaranteed non-None by the call chain; fix the annotation instead
if x is not None - Do not use conditional inheritance patterns; annotate instead
self
- Never use
-
Organization:
- Keep shared Protocols and type aliases in
src/transformers/_typing.py - Import type-only symbols under to avoid circular deps
if TYPE_CHECKING: - Use for PEP 604 syntax (
from __future__ import annotations)X | Y
- Keep shared Protocols and type aliases in
-
Verify and close the PR loop:
- Re-run on the same
ty check<target> - Re-run to confirm the type/model-rules step passes
make typing - If working toward merge readiness, run
make check-repo - Ensure runtime behavior did not change and run relevant tests
- Re-run
-
Update CI coverage when adding new typed areas:
- Update in
ty_check_dirsto include newly type-checked directories.Makefile
- Update
-
从失败运行中确定范围:
- 如果已有或CI输出,提取失败的文件/模块路径。
make typing - 如果没有,运行:
bash
make typing - 选择覆盖所有失败项的最窄目标。
- 如果已有
-
针对目标运行以获取聚焦的基准:
ty checkbashty check --respect-ignore-files --exclude '**/*_pb*' <target> -
在修复前按类别分类错误:
- 签名上类型注解错误/缺失
- 联合类型上的属性访问(例如)
X | None - 返回宽泛联合类型的函数(例如)
str | list | BatchEncoding - Mixin/协议的self类型问题
- 对象或模块上的动态属性
- 第三方存根缺失(缺少关键字参数、缺少等)
__version__
-
按以下优先级顺序应用修复(从最简单的开始):a. 使用/
isinstance()/if x is None缩小联合类型范围。 这是解决联合类型错误的主要工具。hasattr()可以通过所有这些模式进行范围缩小,包括否定形式:typython# 缩小X | None范围 — 使用`if ...: raise`,绝不要用`assert` if x is None: raise ValueError("x must not be None") x.method() # ty知道此处x是X类型 # 缩小str | UploadFile范围 if isinstance(field, str): raise TypeError("Expected file upload, got string") await field.read() # ty知道此处field是UploadFile类型 # 在函数体早期缩小宽泛联合类型参数 # (常见于接受list | dict | BatchEncoding等类型的方法) if isinstance(encoded_inputs, (list, tuple)): raise TypeError("Expected a mapping, got sequence") encoded_inputs.keys() # ty现在只识别dict/mapping类型b. 使用局部变量帮助ty跟踪闭包中的范围缩小。 当是self.x类型且需要传递给嵌套函数或闭包时,X | None无法跟踪ty始终非None。将其复制到局部变量并缩小局部变量的范围:self.xpythonmanager = self.batching_manager if manager is None: raise RuntimeError("Manager not initialized") # 在嵌套函数中使用`manager`(而非`self.batching_manager`)c. 当中间类型是宽泛联合类型时拆分链式调用。 如果因func().method()返回联合类型而失败,将其拆分:func()python# 错误:ty无法通过链式调用缩小范围 result = func(return_dict=True).to(device)["input_ids"] # 正确:拆分、缩小范围后再链式调用 result = func(return_dict=True) if not hasattr(result, "to"): raise TypeError("Expected dict-like result") inputs = result.to(device)["input_ids"]d. 修复源头的错误类型提示。如果参数被标注为但实际调用时永远不会为X | None,则从提示中移除None。Nonee. 为未标注的属性添加注解。为或其他地方设置的实例变量添加类型注解(例如__init__)。 声明稍后动态设置的类级属性(例如self.foo: list[int] = [],_cache: Cache)。_token_tensor: torch.Tensor | Nonef. 为具有输入依赖返回类型的方法使用。 当方法根据输入类型返回不同类型时(例如使用str和int键的@overload),使用__getitem__分别声明每个签名:@overloadpythonfrom typing import overload @overload def __getitem__(self, item: str) -> ValueType: ... @overload def __getitem__(self, item: int) -> EncodingType: ... @overload def __getitem__(self, item: slice) -> dict[str, ValueType]: ... def __getitem__(self, item: int | str | slice) -> ValueType | EncodingType | dict[str, ValueType]: ... # 实际实现这通过为每个调用模式提供精确的返回类型,消除了使用站点的调用。cast()g. 使容器类泛型以传播值类型。 当像这样的类持有经过转换后类型会改变的值时(例如UserDict后列表变为张量),使类泛型以便方法可以返回缩小后的类型:.to()pythonfrom typing import Generic, overload from typing_extensions import TypeVar _V = TypeVar("_V", default=Any) # default=Any保持现有代码正常工作 class MyDict(UserDict, Generic[_V]): @overload def __getitem__(self, item: str) -> _V: ... # ... def to(self, device) -> MyDict[torch.Tensor]: # .to()后,值为张量 ... return self # type: ignore[return-value](来自default=Any)意味着未参数化的用法如typing_extensions保持为MyDict()——现有代码无需修改。 只有缩小值类型的方法(如MyDict[Any])声明特定的返回类型。这消除了所有调用站点的.to()。cast()h. 为mixins使用。当mixin访问宿主类的属性时,在self: "ProtocolType"中定义一个Protocol,并为需要它的方法注解src/transformers/_typing.py。对mixin中的所有方法一致应用此操作。在self下导入以避免循环导入。TYPE_CHECKINGi. 为动态模块属性使用函数(例如TypeGuard,torch.npu,torch.xpu)。不要使用torch.compiler或getattr(torch, "npu"),而是在hasattr(torch, "npu") and torch.npu.is_available()中定义一个类型守卫函数:src/transformers/_typing.pypythondef has_torch_npu(mod: ModuleType) -> TypeGuard[Any]: return hasattr(mod, "npu") and mod.npu.is_available()然后将其用作范围缩小检查:。 在守卫之后,if has_torch_npu(torch): torch.npu.device_count()将模块视为ty,允许无需Any或getattr()的属性访问。有关所有设备后端的现有守卫,请查看cast()。_typing.py类型守卫的关键规则:- 使用(而非Protocol)——这是适用于
TypeGuard[Any]的最简单形式,不会丢失原始模块的已知属性。ty - 必须在条件中直接调用守卫函数才能实现范围缩小。
if不会通过ty条件或and进行范围缩小。if not guard: return - 使用导入守卫(而非通过模块属性
from .._typing import has_torch_xxx)——_typing.has_torch_xxx仅解析直接导入的ty。TypeGuard
j. 为动态模型/配置属性使用/getattr()。 对于运行时注入的字段(例如配置/模型标志),读取时使用setattr(),写入时使用getattr(obj, "field", default)。对于缺少类型存根的第三方包也使用setattr(obj, "field", value)(例如getattr())。 避免使用getattr(safetensors, "__version__", "unknown")这类方式——改用类型守卫(见上文)。getattr(torch, "npu")k. 在使用之前,将# type: ignore作为最后手段。 当你已经通过结构验证确认类型但检查器无法识别时使用:模式匹配的AST节点、已知类型的字典值或已验证的API响应。cast()python# 结构验证确认类型后: stmt = cast(cst.Assign, node.body[0]) annotations = cast(list[Annotation], [])不要为模块属性范围缩小使用——改用类型守卫。 当cast()或泛型可以在源头解决问题时,不要使用@overload。cast()l. 仅在第三方存根缺陷时使用。这指的是第三方包的类型存根错误或不完整,且无法通过范围缩小或类型转换解决的情况。示例:# type: ignore- 运行时存在但存根中缺失的关键字参数
- 存在但未在存根中声明的方法
始终添加具体的错误代码:,不要使用无参数的
# type: ignore[call-arg]。# type: ignore
- 使用
-
绝对不能做的事情:
- 绝不要使用进行类型范围缩小。
assert会被assert剥离,绝不能依赖它来保证正确性。改用python -O。if ...: raise - 绝不要将作为首选方案。先尝试上述所有方法。
# type: ignore - 不要使用访问动态设备后端(
getattr(torch, "backend"),npu,xpu,hpu,musa,mlu,neuron)——使用类型守卫compiler - 不要为模块属性范围缩小使用——使用类型守卫
cast() - 当或泛型可以在源头消除
@overload时,不要使用cast()cast() - 不要仅仅为了满足类型检查器而添加辅助方法或抽象(尤其是仅出现1-2次的情况)
- 不要用特定领域的字段污染基类;使用Protocols
- 不要为调用链中保证非None的值添加守卫;修复注解即可
if x is not None - 不要使用条件继承模式;改为注解
self
- 绝不要使用
-
组织方式:
- 将共享的Protocols和类型别名保存在中
src/transformers/_typing.py - 在下导入仅用于类型的符号以避免循环依赖
if TYPE_CHECKING: - 使用以支持PEP 604语法(
from __future__ import annotations)X | Y
- 将共享的Protocols和类型别名保存在
-
验证并完成PR流程:
- 对同一重新运行
<target>ty check - 重新运行以确认类型/模型规则步骤通过
make typing - 如果准备合并,运行
make check-repo - 确保运行时行为未改变,并运行相关测试
- 对同一
-
添加新类型检查区域时更新CI覆盖率:
- 更新中的
Makefile以包含新的类型检查目录。ty_check_dirs
- 更新