imprecise-annotation (SMP602)

SMP602No fixTypingAll rules

Flags object and Any in annotations, type alias values, and type parameter bounds and defaults. Both accept any value, so the code behind them has to narrow the value at runtime, and a type checker stops checking every value they touch.

Why is this bad?

object accepts anything and permits almost no operations, so the code behind it narrows with isinstance or casts at runtime. Any skips type checking altogether for each value it touches. A precise type moves that check to the type checker and documents what callers pass.

Example

Flagged
1def describe(value: object) -> str:
2    return str(value)
3
4
5CACHE: dict[str, Any] = {}
Preferred
1def describe(value: int | str) -> str:
2    return str(value)
3
4
5CACHE: dict[str, int] = {}

What it does

No fix is offered: the precise type depends on what callers pass.

Active on

  • object in annotations of parameters, return values and variables, including nested uses such as dict[str, object]: each one accepts any value. Both object and builtins.object match.

    • value: object: a parameter annotation.

      Example: reported
      1def describe(value: object) -> str:
      2    return str(value)
      
    • -> object: a return annotation.

      Example: reported
      1def make() -> object:
      2    return 1
      
    • CACHE: dict[str, object]: a variable annotation, with object nested in dict.

      Example: reported
      1CACHE: dict[str, object] = {}
      
  • Any in variable annotations and in nested positions, such as CACHE: Any, dict[str, Any] or -> list[Any]: each one turns off type checking for the values it touches. Spelled Any, typing.Any, an import alias or typing_extensions.Any.

    • CACHE: Any: a variable annotation.

      Example: reported
      1from typing import Any
      2
      3CACHE: Any = {}
      
    • dict[str, t.Any]: a nested position, with Any spelled through the alias t.

      Example: reported
      1import typing as t
      2
      3CACHE: dict[str, t.Any] = {}
      
    • -> list[Any]: a nested position in a return annotation.

      Example: reported
      1from typing import Any
      2
      3
      4def items() -> list[Any]:
      5    return []
      
  • object and Any in type alias values and in type parameter bounds and defaults: a type statement and a type parameter name the type a value or parameter stands for, so the same rules apply to them.

    • type Payload = object: a type alias value.

      Example: reported

      pyproject.toml

      1[project]
      2requires-python = ">=3.12"
      
      1type Payload = object
      
    • def first[T: Any](items: list[T]) -> T: a type parameter bound.

      Example: reported

      pyproject.toml

      1[project]
      2requires-python = ">=3.12"
      
      1from typing import Any
      2
      3
      4def first[T: Any](items: list[T]) -> T:
      5    return items[0]
      
    • class Box[T = object]: a type parameter default, from PEP 696.

      Example: reported

      pyproject.toml

      1[project]
      2requires-python = ">=3.13"
      
      1class Box[T = object]:
      2    pass
      

Exceptions

Parameters of these functions are exempt, since their signatures often have to accept any value. Their return annotations are still checked.

  • Dunder functions and methods, such as __eq__(self, other: object): Python calls them with whatever value the operation received.

    Example: not reported
    1class Point:
    2    def __eq__(self, other: object) -> bool:
    3        return True
    
  • Functions that return TypeIs[...] (PEP 742) or TypeGuard[...] (PEP 647): they exist to narrow a value of unknown type.

    Example: not reported
    1from typing import TypeIs
    2
    3
    4def is_text(value: object) -> TypeIs[str]:
    5    return isinstance(value, str)
    
  • object as the annotation of *args or **kwargs: it is the honest type for arbitrary values, and ANN401 flags Any there unless allow-star-arg-any is set, so object is the one annotation both tools accept. A nested use such as *args: list[object] is still reported.

    Example: not reported
    1def forward(*args: object, **kwargs: object) -> None:
    2    print(args, kwargs)
    
  • An Any that ANN401 reports on a parameter or return, such as def f(value: Any), -> Optional[Any] or def f(value: int | Any): Ruff’s ANN401 reports it, so this rule leaves it alone to avoid a double report, but only when the project’s Ruff configuration selects ANN401. ANN401 reports an Any that is the annotation, or sits in an Optional, a Union, a | or the first argument of Annotated. Ruff’s own suppressions of it, through per-file-ignores or noqa, are respected too.

ANN401 skips the self or cls of a method, @override signatures, overload implementations and, with allow-star-arg-any, *args and **kwargs. This rule still reports the Any in those while ANN401 runs, since no tool would report it otherwise.

Example: not reported

pyproject.toml

1[tool.ruff.lint]
2select = ["ANN401"]
1from typing import Any
2
3
4def run(value: Any) -> None:
5    print(value)
  • object as the return type of a Callable, such as Callable[..., object] or Callable[[int], object], from typing or collections.abc: the callable’s result is ignored, which is what object says there. object in its parameter types is still reported.

    Example: not reported
    1from collections.abc import Callable
    2
    3
    4def run(callback: Callable[[int], object]) -> None:
    5    callback(1)
    
  • String annotations: these are not checked.

    Example: not reported
    1def describe(value: "object") -> str:
    2    return str(value)
    

Ruff overlap

Each tool owns a different part of the Any reports:

  • ANN401 owns the Any it reports on a parameter or return value, when the project’s Ruff configuration selects it: the whole annotation, or an Any inside Optional, Union, | or Annotated. It reports each such annotation once, at its first character, so Any | None and Optional[Any] are one report each, at the start of the annotation, not at the Any.

  • This rule owns everything else: object everywhere except as the annotation of *args or **kwargs, Any in variable annotations, and Any in other nested positions such as list[Any], which ANN401 does not report. It also keeps the Any of the signatures ANN401 skips, such as self: Any or an @override method.

Other tools

  • Ruff: variation of released rule ANN401, which reports a parameter or return annotation that is Any or a union with Any and which this rule leaves to it; this rule covers object and Any nested in other types or in variables (open issue #5871).

  • basedpyright reports every explicit Any as reportExplicitAny. It does not report object, which this rule also flags.