add-or-fix-type-checking

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Add Or Fix Type Checking

添加或修复类型检查

Input

输入

  • <target>
    : module or directory to type-check (if known).
  • Optional
    make typing
    or CI output showing typing failures.
  • <target>
    :要进行类型检查的模块或目录(如果已知)。
  • 可选的
    make typing
    输出或CI输出,显示类型检查失败信息。

Workflow

工作流程

  1. Identify scope from the failing run:
    • If you already have
      make typing
      or CI output, extract the failing file/module paths.
    • If not, run:
      bash
      make typing
    • Choose the narrowest target that covers the failures.
  2. Run
    ty check
    for the target
    to get a focused baseline:
    bash
    ty check --respect-ignore-files --exclude '**/*_pb*' <target>
  3. 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
      __version__
      , etc.)
  4. Apply fixes using this priority order (simplest first):
    a. Narrow unions with
    isinstance()
    /
    if x is None
    /
    hasattr()
    . This is the primary tool for resolving union-type errors.
    ty
    narrows through all of these patterns, including the negative forms:
    python
    # 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 now
    b. Use local variables to help ty track narrowing across closures. When
    self.x
    is
    X | None
    and you need to pass it to nested functions or closures,
    ty
    cannot track that
    self.x
    stays non-None. Copy to a local variable and narrow the local:
    python
    manager = self.batching_manager
    if manager is None:
        raise RuntimeError("Manager not initialized")
    # Use `manager` (not `self.batching_manager`) in nested functions
    c. Split chained calls when the intermediate type is a broad union. If
    func().method()
    fails because
    func()
    returns a union, split it:
    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 typed
    X | None
    but can never be
    None
    when actually called, remove
    None
    from the hint.
    e. Annotate untyped attributes. Add type annotations to instance variables set in
    __init__
    or elsewhere (for example
    self.foo: list[int] = []
    ). Declare class-level attributes that are set dynamically later (for example
    _cache: Cache
    ,
    _token_tensor: torch.Tensor | None
    ).
    f. Use
    @overload
    for methods with input-dependent return types
    . When a method returns different types based on the input type (e.g.
    __getitem__
    with str vs int keys), use
    @overload
    to declare each signature separately:
    python
    from 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 implementation
    This eliminates
    cast()
    calls at usage sites by giving the checker precise return types for each call pattern.
    g. Make container classes generic to propagate value types. When a class like
    UserDict
    holds values whose type changes after transformation (e.g. lists → tensors after
    .to()
    ), make the class generic so methods can return narrowed types:
    python
    from 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
    default=Any
    (from
    typing_extensions
    ) means unparameterized usage like
    MyDict()
    stays
    MyDict[Any]
    — no existing code needs to change. Only methods that narrow the value type (like
    .to()
    ) declare a specific return type. This eliminates
    cast()
    at all call sites.
    h. Use
    self: "ProtocolType"
    for mixins
    . When a mixin accesses attributes from its host class, define a Protocol in
    src/transformers/_typing.py
    and annotate
    self
    on methods that need it. Apply this consistently to all methods in the mixin. Import under
    TYPE_CHECKING
    to avoid circular imports.
    i. Use
    TypeGuard
    functions for dynamic module attributes
    (for example
    torch.npu
    ,
    torch.xpu
    ,
    torch.compiler
    ). Instead of
    getattr(torch, "npu")
    or
    hasattr(torch, "npu") and torch.npu.is_available()
    , define a type guard function in
    src/transformers/_typing.py
    :
    python
    def has_torch_npu(mod: ModuleType) -> TypeGuard[Any]:
        return hasattr(mod, "npu") and mod.npu.is_available()
    Then use it as a narrowing check:
    if has_torch_npu(torch): torch.npu.device_count()
    . After the guard,
    ty
    treats the module as
    Any
    , allowing attribute access without
    getattr()
    or
    cast()
    . See existing guards in
    _typing.py
    for all device backends.
    Key rules for type guards:
    • Use
      TypeGuard[Any]
      (not a Protocol) — this is the simplest form that works with
      ty
      and avoids losing the original module's known attributes.
    • The guard function must be called directly in an
      if
      condition for narrowing to work.
      ty
      does NOT narrow through
      and
      conditions or
      if not guard: return
      .
    • Import guards with
      from .._typing import has_torch_xxx
      (not via module attribute
      _typing.has_torch_xxx
      ) —
      ty
      only resolves
      TypeGuard
      from direct imports.
    j. Use
    getattr()
    /
    setattr()
    for dynamic model/config attributes
    . For runtime-injected fields (for example config/model flags), use
    getattr(obj, "field", default)
    for reads and
    setattr(obj, "field", value)
    for writes. Also use
    getattr()
    for third-party packages missing type stubs (for example
    getattr(safetensors, "__version__", "unknown")
    ). Avoid
    getattr(torch, "npu")
    style — use type guards instead (see above).
    k. Use
    cast()
    as a last resort before
    # type: ignore
    . 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.
    python
    # After structural validation confirms the type:
    stmt = cast(cst.Assign, node.body[0])
    annotations = cast(list[Annotation], [])
    Do not use
    cast()
    for module attribute narrowing — use type guards. Do not use
    cast()
    when
    @overload
    or generics can solve it at the source.
    l. Use
    # type: ignore
    only 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:
    • 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:
      # type: ignore[call-arg]
      , not bare
      # type: ignore
      .
  5. Things to never do:
    • Never use
      assert
      for type narrowing.
      Asserts are stripped by
      python -O
      and must not be relied on for correctness. Use
      if ...: raise
      instead.
    • Never use
      # type: ignore
      as a first resort.
      Exhaust all approaches above first.
    • Do not use
      getattr(torch, "backend")
      to access dynamic device backends (
      npu
      ,
      xpu
      ,
      hpu
      ,
      musa
      ,
      mlu
      ,
      neuron
      ,
      compiler
      ) — use type guards
    • Do not use
      cast()
      for module attribute narrowing — use type guards
    • Do not use
      cast()
      when
      @overload
      or generics can eliminate it at the source
    • 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
      if x is not None
      guards for values guaranteed non-None by the call chain; fix the annotation instead
    • Do not use conditional inheritance patterns; annotate
      self
      instead
  6. Organization:
    • Keep shared Protocols and type aliases in
      src/transformers/_typing.py
    • Import type-only symbols under
      if TYPE_CHECKING:
      to avoid circular deps
    • Use
      from __future__ import annotations
      for PEP 604 syntax (
      X | Y
      )
  7. Verify and close the PR loop:
    • Re-run
      ty check
      on the same
      <target>
    • Re-run
      make typing
      to confirm the type/model-rules step passes
    • If working toward merge readiness, run
      make check-repo
    • Ensure runtime behavior did not change and run relevant tests
  8. Update CI coverage when adding new typed areas:
    • Update
      ty_check_dirs
      in
      Makefile
      to include newly type-checked directories.
  1. 从失败运行中确定范围
    • 如果已有
      make typing
      或CI输出,提取失败的文件/模块路径。
    • 如果没有,运行:
      bash
      make typing
    • 选择覆盖所有失败项的最窄目标。
  2. 针对目标运行
    ty check
    以获取聚焦的基准
    bash
    ty check --respect-ignore-files --exclude '**/*_pb*' <target>
  3. 在修复前按类别分类错误
    • 签名上类型注解错误/缺失
    • 联合类型上的属性访问(例如
      X | None
    • 返回宽泛联合类型的函数(例如
      str | list | BatchEncoding
    • Mixin/协议的self类型问题
    • 对象或模块上的动态属性
    • 第三方存根缺失(缺少关键字参数、缺少
      __version__
      等)
  4. 按以下优先级顺序应用修复(从最简单的开始):
    a. 使用
    isinstance()
    /
    if x is None
    /
    hasattr()
    缩小联合类型范围
    。 这是解决联合类型错误的主要工具。
    ty
    可以通过所有这些模式进行范围缩小,包括否定形式:
    python
    # 缩小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
    无法跟踪
    self.x
    始终非None。将其复制到局部变量并缩小局部变量的范围:
    python
    manager = 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
    ,则从提示中移除
    None
    e. 为未标注的属性添加注解。为
    __init__
    或其他地方设置的实例变量添加类型注解(例如
    self.foo: list[int] = []
    )。 声明稍后动态设置的类级属性(例如
    _cache: Cache
    ,
    _token_tensor: torch.Tensor | None
    )。
    f. 为具有输入依赖返回类型的方法使用
    @overload
    。 当方法根据输入类型返回不同类型时(例如使用str和int键的
    __getitem__
    ),使用
    @overload
    分别声明每个签名:
    python
    from 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()
    后列表变为张量),使类泛型以便方法可以返回缩小后的类型:
    python
    from 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使用
    self: "ProtocolType"
    。当mixin访问宿主类的属性时,在
    src/transformers/_typing.py
    中定义一个Protocol,并为需要它的方法注解
    self
    。对mixin中的所有方法一致应用此操作。在
    TYPE_CHECKING
    下导入以避免循环导入。
    i. 为动态模块属性使用
    TypeGuard
    函数
    (例如
    torch.npu
    ,
    torch.xpu
    ,
    torch.compiler
    )。不要使用
    getattr(torch, "npu")
    hasattr(torch, "npu") and torch.npu.is_available()
    ,而是在
    src/transformers/_typing.py
    中定义一个类型守卫函数:
    python
    def 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
    类型守卫的关键规则
    • 使用
      TypeGuard[Any]
      (而非Protocol)——这是适用于
      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
    之前,将
    cast()
    作为最后手段
    。 当你已经通过结构验证确认类型但检查器无法识别时使用:模式匹配的AST节点、已知类型的字典值或已验证的API响应。
    python
    # 结构验证确认类型后:
    stmt = cast(cst.Assign, node.body[0])
    annotations = cast(list[Annotation], [])
    不要为模块属性范围缩小使用
    cast()
    ——改用类型守卫。 当
    @overload
    或泛型可以在源头解决问题时,不要使用
    cast()
    l. 仅在第三方存根缺陷时使用
    # type: ignore
    。这指的是第三方包的类型存根错误或不完整,且无法通过范围缩小或类型转换解决的情况。示例:
    • 运行时存在但存根中缺失的关键字参数
    • 存在但未在存根中声明的方法 始终添加具体的错误代码:
      # type: ignore[call-arg]
      ,不要使用无参数的
      # type: ignore
  5. 绝对不能做的事情
    • 绝不要使用
      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
  6. 组织方式
    • 将共享的Protocols和类型别名保存在
      src/transformers/_typing.py
    • if TYPE_CHECKING:
      下导入仅用于类型的符号以避免循环依赖
    • 使用
      from __future__ import annotations
      以支持PEP 604语法(
      X | Y
  7. 验证并完成PR流程
    • 对同一
      <target>
      重新运行
      ty check
    • 重新运行
      make typing
      以确认类型/模型规则步骤通过
    • 如果准备合并,运行
      make check-repo
    • 确保运行时行为未改变,并运行相关测试
  8. 添加新类型检查区域时更新CI覆盖率
    • 更新
      Makefile
      中的
      ty_check_dirs
      以包含新的类型检查目录。