🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

Python Type System Deep Dive 2026: PEP 695, Protocols and Generics

⏱️5 min read  ·  920 words

Python’s type system has matured significantly in 2026 with Python 3.12+. PEP 695 (type parameter syntax), PEP 696 (TypeVar defaults), and the new type statement make Python generics more readable. This deep dive covers advanced type patterns that experienced Python developers should know.

New Syntax: type Statement (Python 3.12+)

# Old way (still works)
from typing import TypeAlias, TypeVar
Vector = list[float]
T = TypeVar("T")

# New syntax (Python 3.12+, much cleaner)
type Vector = list[float]
type Matrix = list[Vector]
type JSON = str | int | float | bool | None | list["JSON"] | dict[str, "JSON"]

# Generic type alias
type Stack[T] = list[T]
type Pair[T, U] = tuple[T, U]
type Callback[T] = callable[[T], None]

# Use them
def push[T](stack: Stack[T], item: T) -> Stack[T]:
    return [*stack, item]

numbers: Stack[int] = [1, 2, 3]
result = push(numbers, 4)

Generic Classes and Functions (Python 3.12+)

# Old TypeVar syntax (still valid)
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
    return items[0] if items else None

# New syntax (PEP 695)
def first[T](items: list[T]) -> T | None:
    return items[0] if items else None

def map_list[T, U](items: list[T], fn: callable[[T], U]) -> list[U]:
    return [fn(item) for item in items]

class Stack[T]:
    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        if not self._items:
            raise IndexError("Stack is empty")
        return self._items.pop()

    def peek(self) -> T | None:
        return self._items[-1] if self._items else None

    def __len__(self) -> int:
        return len(self._items)

# Usage
stack: Stack[str] = Stack()
stack.push("hello")
stack.push("world")
print(stack.pop())  # "world"

# Constrained TypeVar
def sort_items[T: (int, str, float)](items: list[T]) -> list[T]:
    return sorted(items)

TypeVar with Default (PEP 696 — Python 3.13+)

from typing import TypeVar

# TypeVar with default
T = TypeVar("T", default=str)

class Container[T = str]:
    def __init__(self, value: T) -> None:
        self.value = value

    def get(self) -> T:
        return self.value

# T defaults to str if not specified
c1: Container = Container("hello")  # T = str (default)
c2: Container[int] = Container(42)  # T = int (explicit)

Protocols — Structural Subtyping

from typing import Protocol, runtime_checkable
from collections.abc import Sequence

@runtime_checkable
class Sizeable(Protocol):
    def __len__(self) -> int: ...

@runtime_checkable
class Comparable[T](Protocol):
    def __lt__(self, other: T) -> bool: ...
    def __le__(self, other: T) -> bool: ...
    def __gt__(self, other: T) -> bool: ...
    def __ge__(self, other: T) -> bool: ...

# Any class with __len__ satisfies Sizeable — no inheritance needed
def count_items(collection: Sizeable) -> int:
    return len(collection)

print(count_items([1, 2, 3]))        # works: list has __len__
print(count_items({"a": 1}))        # works: dict has __len__
print(count_items("hello"))          # works: str has __len__

# isinstance check (requires @runtime_checkable)
print(isinstance([1, 2], Sizeable))  # True

# Protocol with methods
class Serializable(Protocol):
    def to_json(self) -> str: ...
    def to_dict(self) -> dict: ...

    @classmethod
    def from_json(cls, json_str: str) -> "Serializable": ...

Overloads — Multiple Signatures

from typing import overload

@overload
def process(x: int) -> int: ...
@overload
def process(x: str) -> str: ...
@overload
def process(x: list[int]) -> list[int]: ...

def process(x):
    if isinstance(x, int):
        return x * 2
    elif isinstance(x, str):
        return x.upper()
    elif isinstance(x, list):
        return [item * 2 for item in x]

# Type checker knows the return type based on input type
result_int: int = process(5)      # type checker: int
result_str: str = process("hi")   # type checker: str
result_list: list[int] = process([1, 2, 3])  # type checker: list[int]

TypedDict Advanced Patterns

from typing import TypedDict, Required, NotRequired

# Partial TypedDict — some required, some optional
class UserCreate(TypedDict):
    name: Required[str]
    email: Required[str]
    password: Required[str]
    role: NotRequired[str]   # optional

# Inheritance
class UserUpdate(TypedDict, total=False):
    name: str
    email: str
    role: str

# Nested TypedDict
class Address(TypedDict):
    street: str
    city: str
    country: str
    zip_code: NotRequired[str]

class UserWithAddress(TypedDict):
    id: int
    name: str
    address: Address

# Use in function signatures
def create_user(user: UserCreate) -> int:
    return db.insert("users", user)

def update_user(user_id: int, updates: UserUpdate) -> bool:
    return db.update("users", user_id, updates)

ParamSpec — Type-Safe Decorators

from typing import ParamSpec, TypeVar, Callable
import functools

P = ParamSpec("P")
R = TypeVar("R")

def retry(max_attempts: int = 3):
    def decorator(func: Callable[P, R]) -> Callable[P, R]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
            raise RuntimeError("unreachable")
        return wrapper
    return decorator

@retry(max_attempts=3)
def fetch_data(url: str, timeout: int = 30) -> dict:
    return requests.get(url, timeout=timeout).json()

# Type checker knows: fetch_data(url: str, timeout: int = 30) -> dict
# The decorator is fully transparent to the type system

Python’s type system in 2026 with the new PEP 695 syntax is significantly more readable than the TypeVar-based syntax. The key patterns: use Protocols for structural typing, ParamSpec for type-safe decorators, TypedDict for typed dictionaries, @overload for multiple signatures, and the new type statement for clean aliases. Run pyright –strict or mypy –strict to enforce these patterns.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা