skip to content

How do `typing.Any` and `object` differ as annotations to a type checker?

level: juniorimportance: must knowfreq 62%

answer

  1. two ends of the type lattice
  2. one accepts anything, one checks nothing
  3. top type versus escape hatch
  4. isinstance narrows an object parameter
  5. Any is compatible in both directions

basics

~20 s

Any switches checking off for a value: any attribute, call or assignment is allowed. object is the top type, so every value fits it, but you may use only what every object supports until you narrow with isinstance.

solid answer

~50 s

`object` is the top of the type lattice: every value is an `object`, so a parameter annotated `object` accepts anything, but inside the function a checker permits only the operations every object supports — `str()`, `repr()`, `hash()`, `==`, `is`, `isinstance`. To do more you narrow with an `isinstance` check, and the checker follows you into that branch. `typing.Any` is not a member of that lattice at all; it is the instruction to stop checking. Every operation on an `Any` value is accepted, and the *result* of that operation is itself `Any`, so it spreads. Assignment differs too: `Any` is compatible in both directions, while `object` only receives values — you cannot assign an `object` to a `str` variable without narrowing. The rule of thumb: write `object` when you genuinely accept any value, `Any` only when you know something the checker cannot.

code

python · 21 lines
python
from typing import Any


class Widget:
    def render(self) -> str:
        return "widget"


def draw(value: Any) -> None:
    print(value.render())      # a checker allows any attribute on Any


def describe(value: object) -> None:
    print(str(value))          # allowed: every object supports str()
    if isinstance(value, Widget):
        print(value.render())  # allowed only inside the isinstance branch


draw(Widget())
describe(Widget())
describe(42)

go deeper

for a junior

Recall the one-liner: object accepts every value but lets you do almost nothing with it until you check its type; Any accepts every value and lets you do everything. Be able to name isinstance as the way to get from object to a usable type.

for a middle

Explain the mechanics: Any is bidirectionally assignable and its operations yield Any again, while object sits at the top of the lattice and is assignable only in one direction. Show narrowing with isinstance and name what object genuinely supports.

for a senior

Demonstrate the review judgement: which parameters in a real codebase should be object, where Any is honest, and why replacing a failing object annotation with Any usually hides a missing narrow rather than fixing anything.

for a principal

Own the policy angle: what the team's default is at module boundaries, whether dict[str, Any] is allowed to cross into shared code, and how much unchecked surface the codebase can carry before annotations stop buying anything.

Python's type system is *gradual*: it has to describe fully annotated code and code that was never annotated, side by side, in the same program. `typing.Any` and the builtin `object` are the two tools it uses for that, and although both are often described as "anything goes", they sit in completely different places. `object` is the **top of the subtype lattice**. `Any` is **outside the lattice altogether**. ## `object`: the top type Every class in Python inherits from `object`, so every value is an `object`. A parameter annotated `object` therefore accepts anything you pass, and that is the honest way to spell "I take any value". The price is paid inside the function: a static checker will only let you use the operations `object` itself defines. That is a short list — `str()` and `repr()`, `hash()`, `==` and `!=`, `is`, `isinstance()`, and passing the value onward to something else that takes `object`. Everything else is an error: ```python def audit(value: object) -> None: print(str(value)) # fine print(value + 1) # checker error: object has no __add__ print(value.name) # checker error: object has no attribute 'name' ``` To do more you **narrow**. An `isinstance(value, int)` test inside an `if` tells the checker that within that branch the value is an `int`, and arithmetic becomes legal there. This is the whole point of `object`: it forces the question "what is this actually?" to be answered in code, not assumed. Assignment with `object` is one-directional. A `str` can be assigned to a variable declared `object`, because `str` is a subtype of `object`. The reverse is rejected: a value declared `object` cannot be assigned to a `str` variable, because the checker has no evidence it is one. ## `Any`: the escape hatch `Any` is not a type that sits above or below other types. It is a statement that checking is suspended here. It is **bidirectionally compatible**: every type is assignable to `Any`, and `Any` is assignable to every type. That second half is the dangerous one — it is why `Any` never produces an error at an assignment. Inside a function, a value typed `Any` accepts every operation: attribute access, calls, subscription, arithmetic, iteration. And crucially, the *result* of each of those operations is `Any` too. `Any` is contagious in a way `object` is not; it does not merely relax one check, it removes checking from every expression derived from it. Since Python 3.11, `Any` is also a class and may be used as a base: `class Fake(Any): ...` produces a class on which unknown attributes are accepted, which stub and test-double authors occasionally want. ## Where the difference bites: containers The distinction is sharpest with generics. `list[object]` is a list you may read anything out of, but it is not interchangeable with `list[str]` — generics are invariant, and a checker will reject passing a `list[str]` where a `list[object]` is required, because the callee could append an `int` to it. `list[Any]`, by contrast, is compatible with `list[str]` in both directions, because the element type is unchecked. `dict[str, Any]` is the most common shape in real code, and it means "the values are not checked", not "the values may be anything sensible". ## `None` and `NoneType` The leaf next to these two is `None`. In an annotation you write `None` literally — `def save(path: str) -> None:` — and the type it denotes is `types.NoneType`, re-added to the standard library in 3.10, whose only value is the singleton `None`. It is worth naming clearly because people mix it up with the bottom types: `-> None` means "this function *does* return, and the value is `None`". It is an ordinary type with exactly one member, not a type with no members. ## Choosing between them in review The question that settles almost every case is: *do I mean "any value may arrive here", or do I mean "stop checking"?* A logging helper, a generic cache key, an equality shim, a `__eq__` parameter — those accept any value and should say `object`. Deserialized JSON, a plugin loaded by name, a dependency without type stubs — there the checker genuinely cannot know, and `Any` is the honest marker, but it should be confined to the boundary and converted into a declared shape as early as possible. The practical failure mode is reaching for `Any` because `object` produced errors. Those errors were the checker doing its job: it was asking for the `isinstance` check or the narrower annotation that the code was missing.

  • A helper must accept literally any value a caller passes. Which of the two would you annotate its parameter with?
    `object`. It expresses "anything may arrive" while keeping checking switched on inside the body, so the compiler-visible contract stays honest and any real use of the value has to be justified by an `isinstance` narrow. `Any` would accept the same calls but would also silently permit the body to do nonsense with the value, and would leak unchecked types back out through the return.
  • Why is `list[Any]` more permissive than `list[object]` at a call site?
    Generics are invariant, so `list[str]` is not acceptable where `list[object]` is required — the callee could append a non-`str` and corrupt the caller's list. `list[Any]`, though, is compatible with `list[str]` in both directions because the element type is unchecked, so the invariance rule never gets a chance to fire. That is convenient and is exactly why `dict[str, Any]` hides so many real errors.
  • What can you do with `typing.Any` since Python 3.11 that you could not before?
    Use it as a base class. `Any` became a real class in 3.11, so `class Fake(Any): ...` is legal; a checker then accepts arbitrary attribute access on instances of `Fake`. It is a niche tool for test doubles and hand-written stubs that must respond to anything, and it should not appear in ordinary application classes.

object is a door anyone may walk through into a bare waiting room where there is almost nothing you are allowed to touch. Any is the same door with the guard sent home: you may go anywhere beyond it, and nobody is checking what you do.

saying these in an interview costs you the question

  • Says Any and object mean the same thing
  • Thinks object lets you call any method on the value
  • Uses Any to silence errors object correctly raised
  • Believes annotations are enforced by the interpreter at runtime
  • Cannot say that operations on Any produce Any again
  • Claims object is the bottom type of the lattice

context