Generic functions and classes

typing · Practical · 13 min

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 = item

first([1, 2]) is inferred as int, Box("x") as Box[str].

Restrict a type variable:

  • bound: def biggest[T: Comparable](xs: list[T]) -> T accepts T that is the bound or a subtype.
  • constraints: def concat[S: (str, bytes)](a: S, b: S) -> S means exactly str or exactly bytes, 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 bytes

Remember

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

Check yourself

  1. Given def first[T](xs: Sequence[T]) -> T, what does a checker infer for first(['a', 'b'])?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/typing.html#generics