imprecise-annotation (SMP602)¶
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¶
1def describe(value: object) -> str:
2 return str(value)
3
4
5CACHE: dict[str, Any] = {}
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¶
objectin annotations of parameters, return values and variables, including nested uses such asdict[str, object]: each one accepts any value. Bothobjectandbuiltins.objectmatch.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, withobjectnested indict.Example: reported
1CACHE: dict[str, object] = {}
Anyin variable annotations and in nested positions, such asCACHE: Any,dict[str, Any]or-> list[Any]: each one turns off type checking for the values it touches. SpelledAny,typing.Any, an import alias ortyping_extensions.Any.CACHE: Any: a variable annotation.Example: reported
1from typing import Any 2 3CACHE: Any = {}
dict[str, t.Any]: a nested position, withAnyspelled through the aliast.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 []
objectandAnyin type alias values and in type parameter bounds and defaults: atypestatement 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.toml1[project] 2requires-python = ">=3.12"
1type Payload = objectdef first[T: Any](items: list[T]) -> T: a type parameter bound.Example: reported
pyproject.toml1[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.toml1[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) orTypeGuard[...](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)
objectas the annotation of*argsor**kwargs: it is the honest type for arbitrary values, and ANN401 flagsAnythere unlessallow-star-arg-anyis set, soobjectis 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
Anythat ANN401 reports on a parameter or return, such asdef f(value: Any),-> Optional[Any]ordef 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 anAnythat is the annotation, or sits in anOptional, aUnion, a|or the first argument ofAnnotated. Ruff’s own suppressions of it, throughper-file-ignoresornoqa, 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)
objectas the return type of aCallable, such asCallable[..., object]orCallable[[int], object], fromtypingorcollections.abc: the callable’s result is ignored, which is whatobjectsays there.objectin 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
Anyit reports on a parameter or return value, when the project’s Ruff configuration selects it: the whole annotation, or anAnyinsideOptional,Union,|orAnnotated. It reports each such annotation once, at its first character, soAny | NoneandOptional[Any]are one report each, at the start of the annotation, not at theAny.This rule owns everything else:
objecteverywhere except as the annotation of*argsor**kwargs,Anyin variable annotations, andAnyin other nested positions such aslist[Any], which ANN401 does not report. It also keeps theAnyof the signatures ANN401 skips, such asself: Anyor an@overridemethod.
Other tools¶
Ruff: variation of released rule ANN401, which reports a parameter or return annotation that is
Anyor a union withAnyand which this rule leaves to it; this rule coversobjectandAnynested in other types or in variables (open issue #5871).basedpyright reports every explicit
Anyas reportExplicitAny. It does not reportobject, which this rule also flags.