typing module, TypeVar, Generic, Protocol, type aliases
7.0 Prologue
Python's duck typing already provides a form of generic behavior — a function
that works on any type with the right protocol. The typing module (and later
Python 3.12 syntax) gives names to these patterns so that static analyzers can verify
them and IDEs can auto-complete them.
7.1 Built-In Generic Types
Since Python 3.9 the built-in collection types accept subscripts directly as type hints.
No import is needed:
def first(items: list[int]) -> int | None:
return items[0] if items else None
def invert(d: dict[str, int]) -> dict[int, str]:
return {v: k for k, v in d.items()}
For Python 3.7–3.8 use from __future__ import annotations or import
from typing: List[int], Dict[str, int], etc.
7.2 TypeVar
TypeVar defines a type parameter — a placeholder that a static
checker replaces with the concrete type inferred from usage:
from typing import TypeVar
T = TypeVar("T")
def identity(x: T) -> T:
return x
def first_of(items: list[T]) -> T | None:
return items[0] if items else None
reveal_type(first_of([1, 2, 3])) # int | None
reveal_type(first_of(["a", "b"])) # str | None
Constrain a TypeVar to a set of types with bound= or explicit
union: Numeric = TypeVar("Numeric", int, float, complex).
7.3 Generic Classes
Inherit from Generic[T] to make a class parameterize over a type.
Python 3.12 introduces a cleaner class Stack[T]: syntax:
from typing import Generic, TypeVar
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:
if not self._items:
raise IndexError("pop from empty stack")
return self._items.pop()
def __len__(self) -> int:
return len(self._items)
s: Stack[int] = Stack()
s.push(1)
s.push(2)
print(s.pop()) # 2
7.4 Protocol (Structural Subtyping)
A Protocol defines a structural interface: any class that implements the
required attributes and methods satisfies the protocol without explicitly inheriting
from it. This is Python's formalization of duck typing:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("drawing circle")
class Square:
def draw(self) -> None:
print("drawing square")
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # OK — Circle has draw()
render(Square()) # OK — Square has draw()
Decorate a Protocol with @runtime_checkable to support
isinstance(obj, Drawable) checks.
7.5 Type Aliases and NewType
A type alias gives a descriptive name to a complex hint.
Python 3.12 uses type Name = ...; earlier versions use
TypeAlias from typing:
from typing import TypeAlias
Vector: TypeAlias = list[float]
Matrix: TypeAlias = list[Vector]
def dot(a: Vector, b: Vector) -> float:
return sum(x * y for x, y in zip(a, b))
NewType creates a distinct type that behaves like the underlying type at
runtime but is treated as separate by the type checker:
from typing import NewType
UserId = NewType("UserId", int)
def get_user(uid: UserId) -> str: ...
get_user(UserId(42)) # OK
get_user(42) # type error — plain int is not UserId
7.6 Special Forms
Common typing utilities:
Form
Meaning
Optional[X]
X | None
Union[X, Y]
X | Y
Any
escape hatch — compatible with every type
Callable[[A, B], R]
callable with arg types A, B returning R
ClassVar[T]
class variable, not instance variable
Final[T]
constant — cannot be reassigned
Literal["a", "b"]
one of the given literal values
7.7 Epilogue
This chapter covered Python's generic type machinery: TypeVar, Generic classes, Protocol
for structural typing, type aliases, and common special forms. The next chapter surveys
the Python standard library.