skip to content

What does the bracket syntax in `def first[T](xs: list[T]) -> T` declare in Python?

level: juniorimportance: must knowfreq 42%

answer

  1. Modern spelling of a generic function
  2. Introduced by a 3.12 PEP
  3. Replaces a module-level declaration
  4. The class form inherits a base implicitly
  5. Introspect via __type_params__

basics

~20 s

The brackets declare a type parameter T belonging to that function, tying the argument's element type to the return type. Python 3.12 added the syntax in PEP 695; before it you declared T with a module-level TypeVar call.

solid answer

~40 s

`def first[T](xs: list[T]) -> T` declares a generic function: `T` is a type parameter owned by that function, so a checker knows the returned value has the element type of the list passed in. Python 3.12 (PEP 695) introduced it; the pre-3.12 spelling was a separate `T = TypeVar("T")` statement at module level plus the same annotations. At runtime the compiler still builds a `typing.TypeVar`, reachable as `first.__type_params__`, but the name `T` never lands in the module namespace. The same brackets work on classes: `class Box[T]` makes the class generic and implicitly puts `typing.Generic` in its MRO, so listing `Generic[T]` as a base as well raises a `TypeError`. Nothing is enforced at runtime — the brackets carry no checks, only information for a type checker and for introspection.

code

python · 15 lines
python
from typing import Generic, TypeVar

T_old = TypeVar("T_old")

class BoxOld(Generic[T_old]):
    def __init__(self, item: T_old) -> None:
        self.item = item

class Box[T]:
    def __init__(self, item: T) -> None:
        self.item = item

print(Box.__type_params__)
print(Generic in Box.__mro__)
print(T_old in globals().values())

go deeper

for a junior

Be able to read def first[T](xs: list[T]) -> T aloud and say it means the return type matches the list's element type. Know it arrived in Python 3.12 and that the older code you will meet declares T = TypeVar("T") instead.

for a middle

Explain what the compiler builds: a real typing.TypeVar reachable through __type_params__, created in a scope of its own so the name never reaches the module. Know that class Box[T] inherits Generic implicitly and that listing it again raises TypeError.

for a senior

Be ready to lead a conversion: strip module-level TypeVars and explicit Generic[T] bases together, watch for code that reused one shared TypeVar across unrelated generics, and remember the syntax is a parser feature that hard-fails on runtimes older than 3.12.

for a principal

Own the policy call on whether a library may adopt the syntax at all — it sets a 3.12 floor for every consumer, with no __future__ escape hatch. Weigh that against the readability and refactoring benefit of parameters declared where they are used.

## The syntax Since Python 3.12, a square-bracket list directly after the name of a function, class or type alias declares that construct's **type parameters**. `def first[T](xs: list[T]) -> T` says: this function is generic over one type `T`; it takes a list whose elements are `T` and returns a `T`. A checker can therefore conclude that `first([1, 2, 3])` is an `int` and `first(["a"])` is a `str`, without either being written down. Before 3.12, the same function was spelled in two statements: ```python from typing import TypeVar T = TypeVar("T") def first(xs: list[T]) -> T: return xs[0] ``` The annotations were identical; what changed is where `T` comes from. In the old form `T` is an ordinary module-level name bound to a `typing.TypeVar` object — importable, shareable, and easy to reuse by accident. In the new form the parameter is declared where it is used and belongs to that one function. ## What the compiler actually builds The brackets are not decoration. The compiler creates a real `typing.TypeVar` object and attaches the tuple of parameters to the object being defined, under the `__type_params__` attribute: ```python def first[T](xs: list[T]) -> T: return xs[0] print(first.__type_params__) # (T,) print(type(first.__type_params__[0])) # <class 'typing.TypeVar'> ``` A non-generic function has an empty `__type_params__` tuple, so the attribute is a reliable introspection door rather than something that only sometimes exists. The `TypeVar` is created in a hidden scope the compiler generates around the definition, which is why the bare name `T` raises `NameError` in the surrounding module. ## Classes get the same brackets `class Box[T]` declares a generic class. It differs from the function case in one visible way: the class implicitly inherits from `typing.Generic`, so `Generic` appears in `Box.__mro__` without you writing it. Consequently the old habit of listing the base explicitly is now an error: ```python from typing import Generic class Box[T](Generic[T]): # TypeError: Cannot inherit from Generic[...] multiple times. ... ``` That is one of the most common mistakes when converting old code: delete the `Generic[T]` base along with the module-level `TypeVar`. ## What it does not do The brackets add no runtime behaviour. `first([1, "a"])` runs happily; nothing checks that the list is homogeneous, and nothing coerces anything. Generics in Python are erased at runtime in the sense that matters here — the information exists for a static checker, for editors, and for libraries that choose to introspect it. A candidate who claims `def f[T]` enforces the type at call time has the model wrong. A second non-effect: variance is not spelled in the brackets. The pre-3.12 API accepted variance flags on the `TypeVar` call; with the bracket syntax a checker infers variance from how the parameter is used. Whether a container should be treated covariantly is a separate topic, but the *mechanical* point belongs here: there is nowhere in `class Box[T]` to write a variance flag. ## Interoperating with the old spelling Both forms produce `typing.TypeVar` objects, and both are understood by checkers, so a codebase can migrate file by file. What you cannot do is mix them in one declaration: a class written `class Box[T]` cannot also parameterize itself with an imported module-level `TypeVar`, because the bracket form already fixed the class's parameter list. When two functions genuinely need *the same* `T`, the bracket syntax does not express it — each declaration mints its own parameter — and the answer is to make the shared parameter belong to an enclosing generic class or a parameterized type alias. ## Versions to keep straight The bracket syntax on functions, classes and the `type` statement is **3.12** (PEP 695). Giving a type parameter a default (`class Box[T = int]`) is **3.13** (PEP 696). The syntax is a hard compile-time feature, so it cannot be back-ported with `from __future__ import annotations`; a file using it simply will not parse on 3.11 or earlier. That matters when a library still supports older runtimes: the old `TypeVar` spelling remains the portable one.

  • Does a class written `class Box[T]` still need to inherit from `typing.Generic`?
    No. The bracket form makes the class generic and puts `typing.Generic` in its MRO for you. Writing `class Box[T](Generic[T])` raises `TypeError: Cannot inherit from Generic[...] multiple times.` When converting pre-3.12 code, delete both the module-level `TypeVar` and the explicit `Generic[T]` base in the same edit.
  • How do you see the type parameters of a function or class at runtime?
    Read `__type_params__`. It is a tuple of `typing.TypeVar` objects on any function, class or `type` alias declared with brackets, and an empty tuple on everything else, so you can introspect it unconditionally. The parameter names are not otherwise reachable — the bare name raises `NameError` outside the declaration.
  • If `def f[T]` and `def g[T]` appear in one module, do they share a type parameter?
    No. Each declaration creates its own `typing.TypeVar` object; the shared spelling is a coincidence of naming. That is a real change from the pre-3.12 style, where both functions annotated with the same module-level object. If two functions must genuinely range over the same parameter, hang it on an enclosing generic class or a parameterized `type` alias.

The old TypeVar was a public sign hung in the hallway that any room could point at; the brackets nail the sign to the door of the room that uses it.

saying these in an interview costs you the question

  • Reads `def f[T]` as subscripting or indexing the function
  • Says T must still be declared with TypeVar at module level
  • Adds an explicit Generic[T] base to a class written class C[T]
  • Claims the brackets check argument types at call time
  • Thinks the syntax works on any Python 3.x release
  • Believes `from __future__ import annotations` back-ports it

context