Python Story

Chapter #7 – Generics

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
Anyescape 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.

7.8 References

typing module — python.org
Generics — mypy docs
PEP 544 — Protocols
PEP 695 — Type Parameter Syntax (3.12)