Generics keep precise types flowing through containers and helpers instead of collapsing to Any.
A function like first(items) works for any element type. Typing it as list[Any] -> Any loses information. A type variable links input and output: first takes list[T] and returns T, whatever T is at the call site.
Python 3.12 added dedicated syntax (PEP 695):
def first[T](items: Sequence[T]) -> T:
return items[0]
class Box[T]:
def __init__(self, item: T) -> None:
self.item = itemfirst([1, 2]) is inferred as int, Box("x") as Box[str].
Restrict a type variable:
- bound:
def biggest[T: Comparable](xs: list[T]) -> TacceptsTthat is the bound or a subtype. - constraints:
def concat[S: (str, bytes)](a: S, b: S) -> Smeans exactlystror exactlybytes, never mixed.
Before 3.12 you declared T = TypeVar("T") at module level and subclassed Generic[T]. That style still works and is everywhere in existing code, so recognise both. The new syntax scopes T to the function or class and infers variance automatically.
Use generics when a type appears in two or more places in a signature; a single T used once is just object.
Example
from collections.abc import Sequence
def first[T](items: Sequence[T]) -> T:
return items[0]
def longest[S: (str, bytes)](a: S, b: S) -> S: # constrained: str or bytes
return a if len(a) >= len(b) else b
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
n = first([10, 20]) # checker: int
s = Stack[str]()
s.push("a")
print(n + 1, s.pop().upper(), longest("ab", "c")) # 11 A ab
print(Stack.__type_params__) # (T,)
# s.push(3) -> checker error: expected "str"
# longest("a", b"b") -> checker error: mixed str and bytesRemember
- def f[T](x: T) -> T links parameter and return types
- class Box[T] makes a generic class; Box[int] specialises it
- Bounds (T: Base) and constraints (T: (A, B)) restrict T
Watch out for
- Using a type variable only once in a signature; it adds nothing over object.