Type Variables & Generics
TypeVar and Generic let a function or class be written once but type-checked precisely for whatever concrete type it's used with — the mechanism behind list[int] itself.
Generic code operates the same way regardless of the specific type involved — a function that returns the first element of a list works identically whether the list holds ints or strings. Type variables let a static type checker express and verify that relationship precisely: 'whatever type goes in here is the same type that comes out there', without pinning the function or class to one concrete type. This is the same mechanism the standard library itself uses to make list[int] and list[str] behave differently under type checking despite sharing one list implementation.
The Problem: Untyped Generic Functions
Consider a function that returns the first item of any list. Hinting its parameter as list and return as object (or Any) type-checks, but throws away the fact that the return type is always exactly the list's element type — a type checker using object would then flag any specific use of the returned value as unsafe.
The hint 'list -> object' is technically correct but useless: it loses the relationship between what went in and what comes out, forcing callers to re-check/cast the result manually.
def first_untyped(items: list) -> object:
return items[0]
result = first_untyped([1, 2, 3])
# a type checker only knows result is 'object' here — even though it's
# obviously an int at runtime, nothing in the hint expresses that linkTypeVar: Declaring a Type Variable
typing.TypeVar("T") creates a type variable object, conventionally assigned to a single-uppercase-letter name matching the string passed in. Using it in multiple places within one function's signature tells a type checker those places must all resolve to the SAME concrete type for any one call.
For each call, a type checker infers T from the argument (list[int] binds T=int) and applies that same binding to the return type, correctly inferring nums as int and words as str.
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
nums = first([1, 2, 3]) # T is bound to int -> nums: int
words = first(["a", "b"]) # T is bound to str -> words: strGeneric Functions with Multiple Type Variables
A signature can use more than one TypeVar when different parts of it are independently generic — each distinct TypeVar can resolve to a different concrete type per call, while uses of the SAME TypeVar within one call must still agree.
K is bound to str and V to int for this call; the return type tuple[V, K] is then resolved to tuple[int, str], correctly reflecting that swap_pair reverses the pair's order.
from typing import TypeVar
K = TypeVar("K")
V = TypeVar("V")
def swap_pair(key: K, value: V) -> tuple[V, K]:
return (value, key)
result = swap_pair("age", 30) # inferred as tuple[int, str]
print(result) # (30, 'age')Generic Classes
A user-defined class becomes generic by inheriting from typing.Generic[T] (using one or more TypeVars), which enables subscript syntax on the class itself (Stack[int]) exactly like the built-in list[int].
Stack[int] fixes T=int for that specific instance's type; a checker then verifies push() only accepts ints and pop() always returns int for int_stack specifically.
from typing import TypeVar, Generic
T = TypeVar("T")
class Stack(Generic[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()
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
print(int_stack.pop()) # 2, and a type checker knows this is an intBounds: Restricting a TypeVar to a Type Hierarchy
TypeVar("T", bound=SomeType) restricts T to SomeType or any of its subclasses — useful when the generic code needs to rely on behavior/methods that only that family of types guarantees (e.g. comparison operators, a specific base class's interface), while still allowing any specific subclass to flow through precisely.
bound=Animal lets announce() call .speak() (guaranteed by Animal) on any argument, while still preserving the MORE SPECIFIC subclass (Dog) in the return type rather than widening it to Animal.
from typing import TypeVar
class Animal:
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
return "Woof"
A = TypeVar("A", bound=Animal)
def announce(animal: A) -> A:
print(animal.speak())
return animal
fido = announce(Dog()) # a type checker knows fido is specifically Dog, not just AnimalConstraints: Restricting a TypeVar to a Fixed Set of Types
TypeVar("T", TypeA, TypeB, ...) (listing types directly, not via bound=) restricts T to EXACTLY one of the listed types (not their subclasses generally) — different from bound, which accepts any subtype of one hierarchy. Constraints are less commonly needed than bounds but are useful for a small, closed set of unrelated valid types.
The constrained TypeVar forces both arguments (and the return type) to agree on being either str throughout or bytes throughout for any one call — mixing them is a static error.
from typing import TypeVar
StrOrBytes = TypeVar("StrOrBytes", str, bytes)
def concat(a: StrOrBytes, b: StrOrBytes) -> StrOrBytes:
return a + b
print(concat("foo", "bar")) # 'foobar'
print(concat(b"foo", b"bar")) # b'foobar'
# concat("foo", b"bar") # a type checker rejects mixing str and bytes in one callVariance Concepts: Why It Matters for Generic Containers
Variance describes how subtyping between parameterized types relates to subtyping of their type parameters. If Dog is a subtype of Animal, is Container[Dog] a subtype of Container[Animal]? The answer depends on whether the container is read-only (covariant), write-only (contravariant), or both read and write (invariant) — mutable containers like a user-defined mutable Stack are generally invariant, because allowing Stack[Dog] to be used where Stack[Animal] is expected would let unrelated animal types be pushed into what's actually a Dog-only stack.
This is a conceptual summary, not runnable code: '<:' means 'is a subtype of'. Most everyday generic classes you write (mutable containers) are invariant by default, which is usually the safe, correct behavior.
TypeVar accepts covariant=True or contravariant=True keyword arguments to declare non-default variance explicitly for advanced cases (e.g. designing a read-only generic container). This is a deep, ecosystem-tool-dependent topic; the concept is introduced here at a conceptual level rather than exhaustively.
Covariant (read-only, e.g. an immutable container):
Dog <: Animal implies ReadOnlyBox[Dog] <: ReadOnlyBox[Animal]
Contravariant (write-only / consumer position, e.g. a callback parameter):
Dog <: Animal implies Consumer[Animal] <: Consumer[Dog]
Invariant (read AND write, e.g. a typical mutable container):
Dog <: Animal implies NEITHER Box[Dog] <: Box[Animal] NOR the reversePractical Usage: Generic Container and Repository Patterns
Generics are most valuable for reusable containers, repositories, and utility functions meant to work uniformly across many concrete types while still giving precise type information back to each specific caller — exactly the role list, dict, and set play in the standard library itself.
Repository[User] is a single generic class reused for any entity type; a type checker tracks that .get() on a Repository[User] returns User | None specifically, not a generic/unknown object.
from typing import TypeVar, Generic
T = TypeVar("T")
class Repository(Generic[T]):
def __init__(self) -> None:
self._items: dict[int, T] = {}
self._next_id = 1
def add(self, item: T) -> int:
item_id = self._next_id
self._items[item_id] = item
self._next_id += 1
return item_id
def get(self, item_id: int) -> T | None:
return self._items.get(item_id)
class User:
def __init__(self, name: str) -> None:
self.name = name
users: Repository[User] = Repository()
uid = users.add(User("Ana"))
found = users.get(uid) # type checker knows this is User | NoneKey Takeaways
TypeVar creates a placeholder type that a checker binds to a concrete type per call, linking related positions in a signature.
A class becomes generic by inheriting typing.Generic[T], enabling subscript syntax like MyClass[int].
bound=SomeType restricts a TypeVar to that type or its subclasses, while preserving the specific subclass in results.
Listing explicit types (TypeVar('T', A, B)) constrains T to exactly one of those types per call, not a broader hierarchy.
Variance (covariant/contravariant/invariant) governs how subtyping of a container relates to subtyping of its type parameter; mutable containers are invariant by default.
None of this affects runtime execution — it exists purely to let static type checkers verify generic code precisely.
from typing import TypeVar T = TypeVar("T") def combine(a: T, b: T) -> T: return a + b # T isn't guaranteed to support '+' at all from typing import TypeVar Addable = TypeVar("Addable", int, float, str) def combine(a: Addable, b: Addable) -> Addable: return a + b # constrained to types that do support '+'
An unconstrained TypeVar makes no promise about what operations the type supports; using '+' on a bare T is only safe with a bound or constraint that guarantees addition is defined, or a Protocol (a related, more advanced typing concept).
from typing import TypeVar, Generic T = TypeVar("T") class Box: # forgot to inherit Generic[T] def __init__(self, item: T) -> None: self.item = item b: Box[int] = Box(5) # error: Box is not subscriptable from typing import TypeVar, Generic T = TypeVar("T") class Box(Generic[T]): def __init__(self, item: T) -> None: self.item = item b: Box[int] = Box(5)
Using a TypeVar inside a class's methods does not automatically make the class generic; it must explicitly inherit from Generic[T] (or use the modern class Box[T] syntax on 3.12+) to support subscripting like Box[int].
from typing import TypeVar T = TypeVar("T", int, str) S = TypeVar("S", bound=int) def pick(a: T, b: T) -> T: # reusing the SAME constrained TypeVar across unrelated calls doesn't mix well with bound thinking return a from typing import TypeVar Number = TypeVar("Number", bound=int) # accepts int and its subtypes (e.g. bool) def pick(a: Number, b: Number) -> Number: return a
Confusing bound (any subtype of one type) with constraints (one of a fixed, closed list of unrelated types) leads to overly restrictive or overly permissive signatures; pick the mechanism that matches the actual requirement.
What does it mean for a type checker to 'bind' a TypeVar during a specific function call?
It means the checker infers a single concrete type for that TypeVar from the arguments passed at that call site, then uses that same concrete type everywhere else the TypeVar appears in the signature, including the return type.
What must a user-defined class do to support subscript syntax like MyClass[int]?
Inherit from typing.Generic parameterized with one or more TypeVar objects (e.g. class MyClass(Generic[T])), or on 3.12+, use the newer built-in generic class syntax that achieves the same thing.
What's the difference between TypeVar('T', bound=Animal) and TypeVar('T', Dog, Cat)?
bound=Animal accepts Animal or ANY of its subclasses (an open-ended hierarchy) while preserving the specific subclass in results; TypeVar('T', Dog, Cat) constrains T to EXACTLY Dog or exactly Cat per call, a fixed closed list, not any arbitrary subclass of some common ancestor.
Why is a typical mutable generic container (like a user-defined Stack) considered invariant rather than covariant?
Because it supports both reading and writing; if Stack[Dog] were treated as compatible with Stack[Animal], code expecting Stack[Animal] could push a Cat into what's actually backed by a Dog-only stack, violating type safety — invariance prevents that unsound substitution.
What problem does an unconstrained, unbound TypeVar have if the generic function's body tries to call a method or operator on values of that type?
There's no guarantee the concrete type bound to that TypeVar actually supports that method or operator, since T could be bound to any type at all; a bound or constraint (or a Protocol) is needed to make such operations statically safe.
Does using TypeVar and Generic change how a function or class behaves at runtime?
No — like all type hints, they are purely for static analysis; the runtime interpreter does not enforce, check, or alter behavior based on TypeVar bindings or Generic subscripting.
In def first(items: list[T]) -> T, what relationship between the parameter and return type does the shared TypeVar T express that a plain list -> object hint could not?
It expresses that the return value's type is always exactly the SAME as the list's element type for any one call — list[int] in implies T=int out, list[str] in implies T=str out — a link that a non-generic hint using object or Any cannot represent at all.
Write a generic function last_two(items: list[T]) -> tuple[T, T] that returns the last two elements of a list with at least two items.
from typing import TypeVar T = TypeVar("T") def last_two(items: list[T]) -> tuple[T, T]: if len(items) < 2: raise ValueError("need at least two items") return (items[-2], items[-1]) print(last_two([1, 2, 3])) # (2, 3) print(last_two(["a", "b", "c"])) # ('b', 'c')
T links the list's element type to both positions of the returned tuple, so a type checker infers tuple[int, int] for the first call and tuple[str, str] for the second.
What does this print?
2 1. swap() constructs a new Pair with first and second reversed; T is bound to int for this Pair instance throughout, so both the original and swapped Pair are Pair[int].
Write a generic class ReadOnlyBox (using Generic[T]) that stores a value at construction and exposes it only via a .get() -> T method, with no way to change it afterward.
from typing import TypeVar, Generic T = TypeVar("T") class ReadOnlyBox(Generic[T]): def __init__(self, value: T) -> None: self._value = value def get(self) -> T: return self._value box: ReadOnlyBox[str] = ReadOnlyBox("secret") print(box.get()) # secret
ReadOnlyBox(Generic[T]) parameterizes the class by T; the only public method, get(), returns T, and there is no setter, matching the 'read-only' description — a container shaped like this is a natural candidate for covariance in more advanced typing designs.
What does this print, and why is bound=int relevant given that bool was passed in?
2 <class 'int'>. bool is a subclass of int in Python, so it satisfies bound=int; True * 2 evaluates to the int 2 (multiplying a bool by an int produces an int, not a bool), which is exactly what a type checker using bound=int (accepting int and its subtypes) would allow for this call.
from typing import TypeVar, Generic
T = TypeVar("T")
class Pair(Generic[T]):
def __init__(self, first: T, second: T) -> None:
self.first = first
self.second = second
def swap(self) -> "Pair[T]":
return Pair(self.second, self.first)
p = Pair(1, 2)
q = p.swap()
print(q.first, q.second)from typing import TypeVar
T = TypeVar("T", bound=int)
def double(x: T) -> T:
return x * 2
result = double(True)
print(result, type(result))