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:

  1. type statement — for creating type aliases
  2. [T] syntax on classes — for generic classes
  3. [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 TypeVar with 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.

pythontypingpython312type-aliases

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.