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:
if x is None:
raise ValueError("x must not be None")
x.method()
if isinstance(field, str):
raise TypeError("Expected file upload, got string")
await field.read()
if isinstance(encoded_inputs, (list, tuple)):
raise TypeError("Expected a mapping, got sequence")
encoded_inputs.keys()
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:
manager = self.batching_manager
if manager is None:
raise RuntimeError("Manager not initialized")
c. Split chained calls when the intermediate type is a broad union.
If func().method() fails because func() returns a union, split it:
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. 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:
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]:
...
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:
from typing import Generic, overload
from typing_extensions import TypeVar
_V = TypeVar("_V", default=Any)
class MyDict(UserDict, Generic[_V]):
@overload
def __getitem__(self, item: str) -> _V: ...
def to(self, device) -> MyDict[torch.Tensor]:
...
return self
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:
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.
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.