Library authors and large codebases need precise types that express real relationships between values.
Generics connect types in a signature. With the 3.12+ syntax (PEP 695) type parameters go in brackets: def first[T](items: Sequence[T]) -> T: says the return type is the element type. Classes take them too: class Stack[T]:, and you can bound or constrain them: [T: Hashable], [N: (int, float)]. Python 3.13 adds defaults: class Box[T = int]:. The type statement declares aliases, even recursive ones: type JSON = dict[str, JSON] | list[JSON] | str | int | float | bool | None.
Protocols describe types structurally. A class satisfies class SupportsClose(Protocol): def close(self) -> None: ... just by having a matching close method — no inheritance needed. This is static duck typing and usually beats requiring an ABC. Add @runtime_checkable to allow isinstance checks (which only test method presence).
Other tools you'll meet:
TypedDicttypes dict-shaped data like JSON, withRequired/NotRequiredand 3.13'sReadOnlyLiteral["r", "w"]restricts to specific values@overloaddeclares several signatures where the return type depends on argumentsSelffor methods returning their own class;ParamSpecfor typing decoratorsTypeIs(3.13) andTypeGuardfor user-defined narrowing functionsFinal,ClassVar,@override(3.12),@warnings.deprecated(3.13)
Remember these are all for the checker. At runtime, annotations are still stored but not enforced; Python 3.14 makes their evaluation lazy by default, while in 3.13 from __future__ import annotations gives similar deferral.
Example
from collections.abc import Sequence
from typing import Protocol, TypedDict, Literal, TypeIs
def first[T](items: Sequence[T]) -> T: # generic function (3.12+)
return items[0]
class SupportsArea(Protocol):
def area(self) -> float: ...
class Square: # no inheritance needed
def __init__(self, s: float) -> None: self.s = s
def area(self) -> float: return self.s ** 2
def total_area(shapes: Sequence[SupportsArea]) -> float:
return sum(s.area() for s in shapes)
class User(TypedDict):
name: str
role: Literal["admin", "member"]
def is_str_list(val: list[object]) -> TypeIs[list[str]]: # 3.13
return all(isinstance(x, str) for x in val)
type Pair[T] = tuple[T, T] # generic type alias
print(first("abc"), total_area([Square(2), Square(3)]), is_str_list(["a"])) # a 13 True
print(User(name="ana", role="admin"), Pair[int]) # a plain dict; Pair[int]Remember
def f[T](x: T) -> Tandclass C[T]declare generics (3.12+)- Protocols give structural typing without inheritance
- TypedDict, Literal, overload and TypeIs express precise contracts
Watch out for
- Using
Anyto silence the checker where a TypeVar or Protocol would express the real relationship.