Python Type Aliases (PEP 695) — Core Concepts
What PEP 695 introduced
PEP 695 (Python 3.12) added three pieces of syntax that simplify how you work with Python’s type system:
typestatement — for creating type aliases[T]syntax on classes — for generic classes[T]syntax on functions — for generic functions
Together, they replace the TypeVar, Generic, and TypeAlias imports that made generic code verbose.
The type statement
Before (3.10-3.11)
from typing import TypeAlias
# Simple alias
UserID: TypeAlias = int
# Parameterised alias
from typing import TypeVar
T = TypeVar("T")
Result: TypeAlias = tuple[T, str | None]
After (3.12+)
# Simple alias
type UserID = int
# Parameterised alias — no TypeVar import needed
type Result[T] = tuple[T, str | None]
The type statement is a proper language construct, not a variable assignment. This means:
- Type checkers unambiguously know it’s an alias
- The right-hand side is lazily evaluated (forward references work naturally)
- The type parameters have proper scoping
Generic classes
Before
from typing import TypeVar, Generic
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")
class Stack(Generic[T]):
def push(self, item: T) -> None: ...
def pop(self) -> T: ...
class Mapping(Generic[K, V]):
def get(self, key: K) -> V | None: ...
After
class Stack[T]:
def push(self, item: T) -> None: ...
def pop(self) -> T: ...
class Mapping[K, V]:
def get(self, key: K) -> V | None: ...
No imports. The type parameters are declared inline and scoped to the class body.
Generic functions
Before
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
def pair(a: T, b: T) -> tuple[T, T]:
return (a, b)
After
def first[T](items: list[T]) -> T:
return items[0]
def pair[T](a: T, b: T) -> tuple[T, T]:
return (a, b)
Bounds and constraints
Type parameters can be bounded or constrained:
# Upper bound — T must be a subtype of Comparable
class SortedList[T: Comparable]:
def add(self, item: T) -> None: ...
# Constraint — T must be exactly int or str (not subtypes)
def convert[T: (int, str)](value: T) -> T: ...
Lazy evaluation
The right-hand side of type statements is evaluated lazily — only when __value__ is accessed:
type Tree = Node | Leaf # Works even though Node and Leaf aren't defined yet
class Node:
left: Tree
right: Tree
value: int
class Leaf:
value: int
# Access triggers evaluation
print(Tree.__value__) # Node | Leaf
This eliminates the need for from __future__ import annotations or string-quoted forward references in many cases.
Scoping
Type parameters create a new annotation scope:
T = int # Module-level T
class Box[T]: # This T is different — scoped to Box
value: T # Refers to the Box-scoped T, not int
print(T) # Still int — the module-level T is unaffected
This fixes a long-standing issue where TypeVar("T") at module level could accidentally conflict with other uses of T.
How type checkers handle it
Both mypy and pyright fully support PEP 695 syntax. They:
- Infer variance from usage (covariant, contravariant, or invariant)
- Resolve forward references through lazy evaluation
- Enforce bounds and constraints
- Support mixing old-style
TypeVarwith new-style[T]syntax
Common misconception
“The type statement just assigns a variable.” It creates a TypeAliasType object, which is fundamentally different from a plain variable assignment. Type checkers treat them differently — a TypeAliasType supports lazy evaluation, proper generic parameterisation, and won’t be confused with a runtime variable.
The one thing to remember: PEP 695 made generics a first-class part of Python’s syntax — no more import rituals, no more module-level TypeVar pollution, just clean declarations.
See Also
- Python 310 New Features Python 3.10 gave programmers a shape-sorting machine, friendlier error messages, and cleaner ways to say 'this or that' in type hints.
- Python 311 New Features Python 3.11 made everything faster, error messages smarter, and let you catch several mistakes at once instead of stopping at the first one.
- Python 312 New Features Python 3.12 made type hints shorter, f-strings more powerful, and started preparing Python's engine for a world without the GIL.
- Python 313 New Features Python 3.13 finally lets multiple tasks run at the same time for real, added a speed booster engine, and gave the interactive prompt a colourful makeover.
- Python Exception Groups Python's ExceptionGroup is like getting one report card that lists every mistake at once instead of stopping at the first one.