PEP 695: Generics and Type Aliases in Python 3.12
Python 3.12 introduces PEP 695 (Type Parameter Syntax), which
fundamentally modernizes how generic classes, functions, and type
aliases are defined. By adding native type parameter syntax and the
dedicated type soft keyword, Python eliminates the need for
verbose boilerplate like typing.TypeVar and
typing.Generic. This article explores how PEP 695
streamlines type annotations, enables cleaner generic class syntax, and
introduces lazily evaluated type aliases for more intuitive and
maintainable code.
The New
type Statement for Type Aliases
Prior to Python 3.12, type aliases were declared using standard
variable assignments or the typing.TypeAlias annotation
introduced in PEP 613:
from typing import TypeAlias
# Old approach
Point: TypeAlias = tuple[float, float]While functional, this approach had limitations, particularly with forward references and circular dependencies, as the right-hand side was evaluated at runtime immediately upon definition.
PEP 695 replaces this pattern with a native type
statement:
# Python 3.12+ approach
type Point = tuple[float, float]The type keyword creates an instance of
typing.TypeAliasType. Crucially, the value of the alias is
evaluated lazily. This lazy evaluation allows self-referencing and
recursive data structures to be defined naturally without string-based
forward references:
type JSON = dict[str, JSON] | list[JSON] | str | int | float | bool | NoneGeneric Type Aliases
Without TypeVar
Under the old system, creating a generic type alias required manually
declaring type variables with TypeVar:
from typing import TypeVar
T = TypeVar("T")
OldListOrSet = list[T] | set[T]With PEP 695, generic parameters are declared directly inside brackets immediately following the alias name:
type ListOrSet[T] = list[T] | set[T]
type Mapping[K, V] = dict[K, V]The type parameter T is automatically scoped strictly to
the alias definition, removing the need to manage global or module-level
TypeVar instances.
Simplified Generic Class Definitions
PEP 695 eliminates the need to inherit from
typing.Generic. Classes now declare their type parameters
directly in their signature.
Old 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)Python 3.12 Syntax:
class Stack[T]:
def __init__(self) -> None:
self.items: list[T] = []
def push(self, item: T) -> None:
self.items.append(item)The runtime automatically creates a scoped TypeVar for
T and makes the class subscriptable, removing runtime
inheritance overhead and reducing import clutter.
Type Parameter Bounds and Constraints
Bounds and constraints are also integrated directly into the inline
syntax using a colon : instead of passing arguments to
TypeVar.
- Upper bounds restrict the type to a subclass of a
specified type:
class NumericStorage[T: (int, float)]: ... - Constrained types limit the variable to specific
types:
class TextContainer[T: str | bytes]: ...
Scoping and Runtime Introspection
A major technical improvement of PEP 695 is type parameter scoping.
Previously, TypeVar("T") lived in the module's global
namespace, which could lead to accidental reuse across unrelated classes
and functions. Under PEP 695, type parameters exist inside a dedicated
annotation scope:
- Type parameters declared on a class or alias are accessible only within that definition's scope.
- The runtime stores these parameters in a
__type_params__tuple on the class or alias object, enabling better introspection for static analysis tools, runtime type checkers, and IDEs.
By replacing external imports with dedicated syntax, PEP 695 makes generics a first-class language feature in Python 3.12, resulting in cleaner code, fewer imports, and reduced runtime overhead.