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 | None

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

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:

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.