Why annotate a Python helper with a TypeVar instead of Any?
answer
- It is about keeping information
- One placeholder, two positions
- Any turns checking off downstream
- Argument type flows to the return
- Solved per call site, never at runtime
basics
~20 sA TypeVar is a placeholder that ties the argument type to the return type, so a checker knows a helper called with a list of strings gives back a string. Any severs that link and switches checking off for everything downstream.
solid answer
~50 s`T = TypeVar("T")` declares a type *parameter*: a placeholder that a static checker solves separately at every call site. Because the same `T` appears in the parameter annotation and in the return annotation, calling `first(items: Sequence[T]) -> T` with a `list[str]` yields `str`, and with a `list[int]` yields `int` — the caller's type is preserved. `Any` is the opposite: it is the type that is compatible with everything in both directions, so annotating the return as `Any` means the checker stops complaining about anything you do with the result. The practical rule is that a TypeVar is only useful when it appears at least twice in one signature; a placeholder used once carries no relationship and is just a more expensive `Any`. None of this is enforced at runtime — the annotation is metadata, and the function body runs the same either way.
code
python · 10 linesfrom typing import TypeVar
from collections.abc import Sequence
T = TypeVar("T")
def first(items: Sequence[T]) -> T:
return items[0]
print(first([3, 1, 2]) + 1)
print(first(["north", "south"]).upper())go deeper
Be ready to state the one-line difference: a TypeVar keeps the caller's type, Any throws it away. Know that annotations do nothing at runtime and that the placeholder must appear at least twice to be worth writing.
Explain the mechanics: the checker solves the parameter per call site by matching the argument against the annotation, then substitutes into the return. Explain why a bare T restricts the body to object-level operations, and why Any is contagious through downstream expressions.
Show judgement about where Any is a legitimate boundary — deserialised payloads, dynamic plugin edges — and where it is a silenced diagnostic. An interviewer expects you to notice a single Any in a shared helper de-typing every caller of it.
Own the policy angle: which layers of a codebase are allowed Any at all, how strictly the checker is configured per package, and how a team migrates an untyped module without one convenience Any spreading through the call graph.
## What a type parameter actually is `typing.TypeVar` creates a **type parameter**: a named hole in a signature that a static checker fills in per call site. It is a variable whose values are *types*, not objects. Declaring one is a runtime statement like any other: ```python from typing import TypeVar T = TypeVar("T") ``` The string argument is the parameter's name and, by convention and by most checkers' enforcement, must match the variable it is bound to — `T = TypeVar("U")` is a diagnosable error, not a stylistic quirk. ## Why `Any` is not a substitute `Any` is the escape hatch of the type system: it is assignable to every type and every type is assignable to it. A helper annotated `def first(items: Sequence[Any]) -> Any` type-checks against every call, and — the real cost — every *use of its result* also type-checks. Call `.bit_length()` on what is actually a `str` and nothing complains; the error surfaces at runtime, in whatever code path happened to run first. A TypeVar preserves information instead of discarding it: ```python def first(items: Sequence[T]) -> T: return items[0] first([3, 1, 2]) # checker infers int first(["north", "south"]) # checker infers str ``` The checker sees `T` in the parameter position, *solves* it from the argument (`list[str]` matches `Sequence[T]` with `T = str`), and substitutes that solution into the return annotation. Each call is solved independently — nothing is remembered between calls. ## The "appears twice" rule The single most useful heuristic: **a TypeVar earns its place only when it occurs in at least two positions in one signature.** Two parameters, or a parameter and the return, or a parameter and a nested position such as `Sequence[T]` and `T`. Those repetitions are the *relationship* the parameter expresses. ```python def repeat(item: T, times: int) -> list[T]: ... # useful: T links input to output def log(item: T) -> None: ... # pointless: use object ``` A parameter appearing once constrains nothing, and the honest annotation for "I accept anything and look at none of it" is `object` — which, unlike `Any`, still refuses attribute access you have not proved is safe. ## Unbound TypeVars accept anything A bare `TypeVar("T")` is *unbounded*: it can be solved to any type, so inside the function body the checker lets you do only what you can do to `object`. You cannot call `item.label()` or `item + item` on a value typed as a bare `T`, because the next caller might pass something without that operation. Widening what the body may assume is exactly what `bound=` and a value-constraint list are for. ## Runtime versus checking Everything above happens **before** the program runs. At runtime the annotation is just an object stored on the function; `TypeVar` performs no validation, no coercion, and no dispatch. Passing a `str` where the checker inferred `int` raises nothing at the boundary — it fails later, wherever an integer operation actually runs. This trips up candidates who describe generics as "enforcing" a type: Python's generics are entirely erased at execution time, and the value of writing them is that a checker, an editor's completions and the next reader all get the information. ## Generic functions versus generic classes A function using a TypeVar is a **generic function**: it needs no base class and no registration, just the shared placeholder. A generic *class* is the other half of the story — it inherits `Generic[T]` (or a base already parameterised by `T`) so the parameter is fixed once per instance rather than once per call. The two mechanisms use the same `TypeVar` object; what differs is the scope over which the solution holds. ## Where beginners go wrong Three recurring mistakes: reusing one module-level TypeVar in unrelated signatures and assuming that links them (it does not — solving is per call site); expecting `TypeVar` to reject bad arguments at runtime; and reaching for `Any` to silence a checker complaint that a TypeVar would have answered properly. The last one is the expensive one, because `Any` is contagious: it propagates through every expression built from the value, and a single `Any` in a hot helper can quietly de-type a whole call chain.
- When is `object` the better annotation than either a TypeVar or Any?When the function accepts anything and the caller learns nothing type-specific back — a logging or counting helper. `object` says 'any value' while still refusing unproven attribute access, whereas `Any` disables checking on the value entirely. A TypeVar would be noise there, because it appears only once and therefore expresses no relationship.
- Does reusing the same module-level TypeVar in two different functions connect them?No. A TypeVar is solved independently per generic function and per call site; the shared object is just a reusable declaration, not a shared binding. `first(strings)` solving `T = str` says nothing about what `repeat(3, 2)` solves. Only positions inside one signature — or inside one generic class — are linked.
- What can a function body legally do with a value annotated as a bare TypeVar?Only what is valid on `object`: pass it around, store it, compare identity, call `repr()`. Attribute access, arithmetic and iteration are all rejected, because the parameter may be solved to a type lacking them. To assume more you must narrow the parameter with `bound=` or a value-constraint list, or narrow the value with an `isinstance` check inside the body.
A TypeVar is like the x in an algebraic identity: whatever you substitute on the left must come out on the right. Any is a shrug — it accepts every substitution and promises nothing about the result.
saying these in an interview costs you the question
- Claims TypeVar checks or coerces arguments at runtime
- Says Any and object mean the same thing
- Uses a TypeVar that appears only once in a signature
- Thinks the string in TypeVar("T") is arbitrary
- Believes one shared TypeVar links separate functions
- Calls arbitrary methods on a value typed as a bare T