Steven's Knowledge

Type System

Python's gradual type system in depth -- type hints, Protocols, generics (PEP 695), Self, TypedDict, and exhaustiveness checking

Type System

Python's type system is gradual -- you add types incrementally, and a static checker (mypy/pyright) enforces them while the interpreter ignores them. Used well, type hints carry real design intent: structural Protocols, clean generics, and exhaustiveness checks that turn whole classes of bug into compile-time errors.

Type Hints

Python's type system is gradual -- you add types incrementally. Modern Python (3.10+) type hints are expressive:

from typing import TypeVar, Protocol, overload, runtime_checkable

# Protocol: structural subtyping (like Go interfaces)
@runtime_checkable
class Serializable(Protocol):
    def to_dict(self) -> dict: ...

class User:
    def to_dict(self) -> dict:
        return {"name": self.name}

def serialize(obj: Serializable) -> str:
    return json.dumps(obj.to_dict())
# User satisfies Serializable without inheriting from it

# Overload: different return types based on input
@overload
def fetch(id: str, many: Literal[False] = ...) -> User: ...
@overload
def fetch(id: str, many: Literal[True] = ...) -> list[User]: ...

def fetch(id: str, many: bool = False) -> User | list[User]:
    if many:
        return db.find_all(id)
    return db.find_one(id)

# TypeVar with bounds
T = TypeVar("T", bound="BaseModel")

def create_and_save(model_cls: type[T], data: dict) -> T:
    instance = model_cls(**data)
    instance.save()
    return instance

Advanced Type System

Beyond the basics, the type system carries real design intent. Python 3.12's PEP 695 syntax makes generics far cleaner:

# PEP 695 (3.12+): no more explicit TypeVar declarations
class Repository[T]:
    def __init__(self) -> None:
        self._items: dict[str, T] = {}

    def get(self, key: str) -> T | None:
        return self._items.get(key)

def first[T](items: list[T]) -> T | None:
    return items[0] if items else None

# Self (3.11+) for fluent builders -- correct even in subclasses
from typing import Self

class QueryBuilder:
    def where(self, cond: str) -> Self:
        self._conditions.append(cond)
        return self

# TypedDict for structured dict payloads at the boundary
from typing import TypedDict, NotRequired

class UserPayload(TypedDict):
    id: str
    email: str
    nickname: NotRequired[str]   # optional key, not Optional value

# Exhaustiveness checking -- the type checker errors if you miss a case
from typing import assert_never

def area(shape: Circle | Square) -> float:
    match shape:
        case Circle(r): return 3.14159 * r * r
        case Square(s): return s * s
        case _: assert_never(shape)  # adding a new shape becomes a type error

Two distinctions experts hold precisely: T | None (a value that may be None) is not the same as a key that may be absent (NotRequired); and runtime type hints are just annotations -- mypy/pyright enforce them, the interpreter does not. x: int = "no" runs fine.

On this page