A TypeVar is a type variable — a placeholder that lets a function or class
work with any type while preserving it across inputs and outputs. Subclassing
Generic[T] turns a class into a generic container parameterized by that
variable, so a Stack[int] is known to hold and return ints.
from typing import TypeVar, Generic
T = TypeVar("T") # one placeholder type
def first(items: list[T]) -> T: # in and out share T
return items[0]
class Stack(Generic[T]): # generic class
def __init__(self) -> None:
self._items: list[T] = []
def push(self, x: T) -> None:
self._items.append(x)
def pop(self) -> T:
return self._items.pop()
s: Stack[int] = Stack()
s.push(1)
n = s.pop() # type checker knows n is int
Use a TypeVar whenever a relationship between argument and return types must be
captured — def first(items: list) -> object loses that, but list[T] -> T
keeps it. In Python 3.12+ you can write def first[T](items: list[T]) -> T
without the explicit TypeVar.
A plain TypeVar accepts any type. A bound (bound=...) restricts it to a
type and its subclasses, while constraints (TypeVar("T", int, str))
restrict it to a fixed set of specific types. Both let the body safely use the
capabilities implied by the bound.
from typing import TypeVar
class Animal:
def speak(self) -> str: ...
A = TypeVar("A", bound=Animal) # A must be Animal or a subclass
def loudest(animals: list[A]) -> A:
for a in animals:
a.speak() # OK — bound guarantees this method
return animals[0]
Num = TypeVar("Num", int, float) # constrained: ONLY int or float
def double(x: Num) -> Num:
return x * 2
Reach for a bound when "any subtype of X" is acceptable and the body needs X's interface; use constraints when only a handful of unrelated concrete types should be allowed.
A Protocol defines an interface by shape rather than inheritance: any
object that has the right methods/attributes is accepted, even if it never
explicitly subclasses the protocol. This is structural typing ("duck typing")
checked statically — "if it walks like a duck."
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None: ...
def shutdown(resource: SupportsClose) -> None:
resource.close()
class File: # never inherits SupportsClose...
def close(self) -> None: ...
shutdown(File()) # ...but accepted: it has close()
Contrast with nominal typing (the usual class B(A)), where you must declare the
relationship. Protocols decouple the consumer from concrete classes — great for
typing third-party objects you can't modify.
By default a Protocol exists only for static checkers — isinstance() against
it raises TypeError. Decorating it with @runtime_checkable allows
isinstance() / issubclass() checks at runtime, but only for the presence of
the named methods, not their signatures or return types.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Sized(Protocol):
def __len__(self) -> int: ...
isinstance([1, 2, 3], Sized) # True — list has __len__
isinstance(42, Sized) # False — int has no __len__
It's a convenience, not a guarantee: the check confirms a method exists, not that
it takes the right arguments. Prefer static checking; use @runtime_checkable only
when you genuinely need a runtime branch.
Use Callable[[ArgTypes], ReturnType] from typing (or the built-in
collections.abc.Callable). The first element is the list of parameter types,
the second is the return type. Use ... for the parameters when you want to
accept any signature.
from collections.abc import Callable
def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
return fn(a, b)
apply(lambda x, y: x + y, 2, 3) # 5
handler: Callable[..., None] # any args, returns None
no_args: Callable[[], str] # takes nothing, returns str
For more precise signatures (preserving exact parameters of a wrapped function),
ParamSpec exists, but Callable[[...], R] covers the common cases. Type your
callbacks so the checker catches mismatched handlers.
Variance describes whether Container[Subtype] is usable where Container[Supertype]
is expected. Invariant (the default, e.g. list[T]): list[int] is not a
list[str] or a list[object]. Covariant: Tuple[int] is acceptable as
Tuple[object]. The intuition: mutable containers must be invariant for safety;
read-only ones can be covariant.
def total(nums: list[float]) -> float: ...
ints: list[int] = [1, 2]
total(ints) # type ERROR — list is invariant
from collections.abc import Sequence
def total2(nums: Sequence[float]) -> float: ...
total2(ints) # OK — Sequence is covariant (read-only)
Why mutables are invariant: if list[int] were a list[object], a function could
append a str to it, corrupting the original list[int]. Rule of thumb: accept
Sequence/Iterable (covariant, read-only) in parameters to be flexible; reserve
list/dict for when you truly need to mutate.
Declare several TypeVars and use them independently to capture distinct but
related types — for example a key type and a value type, or an input and an output.
Each variable is resolved separately by the type checker.
from typing import TypeVar
from collections.abc import Callable
K = TypeVar("K")
V = TypeVar("V")
R = TypeVar("R")
def get_or(d: dict[K, V], key: K, default: V) -> V: # K and V independent
return d.get(key, default)
def transform(items: list[K], fn: Callable[[K], R]) -> list[R]:
return [fn(x) for x in items] # K in, R out
Rule of thumb: use one TypeVar per independent type relationship — pair them
(like K/V) when a function genuinely ties two types together.
Self (Python 3.11+) is a type that means "the current class", so methods that
return the instance — fluent builders, __enter__, alternative constructors — type
correctly even in subclasses. It replaces the old bound-TypeVar trick.
from typing import Self
class Query:
def where(self, cond: str) -> Self: # returns same type as the caller
...
return self
class AdminQuery(Query):
pass
q = AdminQuery().where("active") # inferred as AdminQuery, not Query
Rule of thumb: annotate "returns itself" methods with Self so chaining and factory
methods keep the subclass type instead of collapsing to the base class.
@overload lets you declare multiple type signatures for one function whose
return type depends on its argument types. You write several @overload stubs (bodies
are ...) followed by a single real implementation — the checker matches callers
against the stubs; the stubs vanish at runtime.
from typing import overload
@overload
def parse(x: int) -> int: ...
@overload
def parse(x: str) -> list[str]: ...
def parse(x): # the only real implementation
return x if isinstance(x, int) else x.split()
n = parse(10) # checker knows: int
parts = parse("a b") # checker knows: list[str]
Rule of thumb: use @overload when one function's return type varies by input
type; keep exactly one implementation below the stubs.
ParamSpec (3.10+) captures an entire parameter list so a decorator can wrap a
function without losing its signature. A plain Callable[..., R] forgets the
arguments; ParamSpec forwards them, keeping arg-level type checking on the wrapper.
from typing import ParamSpec, TypeVar
from collections.abc import Callable
import functools
P = ParamSpec("P")
R = TypeVar("R")
def logged(fn: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(fn)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # same params as fn
print("calling", fn.__name__)
return fn(*args, **kwargs)
return wrapper
@logged
def add(a: int, b: int) -> int: ...
add(1, 2) # still type-checked; add("x") flagged
Rule of thumb: type signature-preserving decorators with Callable[P, R] +
*args: P.args, **kwargs: P.kwargs so wrapped calls keep full type safety.
A Protocol can itself be generic by also subclassing Generic[T] (or using the
3.12 class P[T](Protocol) form). This describes a shape parameterized by a type —
e.g. "anything that produces T values" — combining structural typing with generics.
from typing import Protocol, TypeVar
T = TypeVar("T")
class Producer(Protocol[T]):
def produce(self) -> T: ...
def collect(p: Producer[int]) -> list[int]:
return [p.produce() for _ in range(3)]
class IntGen: # no inheritance needed
def produce(self) -> int: return 7
collect(IntGen()) # accepted: matches Producer[int] structurally
Rule of thumb: parameterize a Protocol when the shape carries a type (a container,
factory, or callback interface) you want checked structurally.
Yes — a Protocol can require attributes/properties by declaring annotated class
variables. Any object with those attributes (however they're implemented — plain
field, property, slot) satisfies the protocol.
from typing import Protocol
class Named(Protocol):
name: str # required attribute, not a method
def greet(obj: Named) -> str:
return f"Hi {obj.name}"
class User: # has a 'name' attribute -> matches
def __init__(self, name: str) -> None:
self.name = name
greet(User("Ada")) # OK
Rule of thumb: list required data as annotated attributes in the Protocol; the
checker accepts any object exposing them, regardless of how they're stored.
Pass covariant=True or contravariant=True when creating the TypeVar
(conventionally named with _co/_contra). Covariant suits producer/read-only
generics (outputs); contravariant suits consumer generics (inputs). The default
is invariant.
from typing import TypeVar, Generic
T_co = TypeVar("T_co", covariant=True)
class Box(Generic[T_co]): # read-only producer
def __init__(self, value: T_co) -> None:
self._value = value
def get(self) -> T_co:
return self._value
b: Box[object] = Box[int](1) # OK — Box[int] usable as Box[object]
A covariant variable must only appear in output positions; using it as a method
argument is a type error. Rule of thumb: mark covariant for read-only producers,
contravariant for write-only consumers, and leave mutable containers invariant.
Python 3.12 added inline type parameters: write def fn[T](...) and
class C[T]: without declaring a separate TypeVar, plus a type statement
for aliases. It's more concise and scopes the variable to the function/class.
# 3.12+ — no `T = TypeVar("T")` needed
def first[T](items: list[T]) -> T:
return items[0]
class Stack[T]:
def push(self, x: T) -> None: ...
type Vector = list[float] # generic-capable alias
type Pair[T] = tuple[T, T]
It coexists with the classic TypeVar style (still required pre-3.12). Rule of thumb:
prefer the inline [T] syntax and type aliases on 3.12+; fall back to explicit
TypeVar/TypeAlias for older runtimes.
Both define an interface, but differ in how conformance is established. An ABC
(abc.ABC) is nominal — a class must explicitly subclass it and implement its
abstract methods. A Protocol is structural — any class with the right shape
matches, no inheritance required.
from abc import ABC, abstractmethod
from typing import Protocol
class WriterABC(ABC): # nominal: must subclass
@abstractmethod
def write(self, data: str) -> None: ...
class WriterProto(Protocol): # structural: just needs write()
def write(self, data: str) -> None: ...
class Console: # matches WriterProto, NOT WriterABC
def write(self, data: str) -> None: print(data)
Use an ABC when you own the hierarchy and want to share implementation or force registration; use a Protocol to type objects you don't control (third-party or duck-typed). Rule of thumb: Protocol for "any object shaped like this", ABC for "a member of this explicit family".
More Type Hints & Typing interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.