Typing and Generics

Medium40 min read

Why Static Typing?

Why Static Typing Matters

The Problem: Dynamic typing lets bugs survive until they hit production. Reading untyped code means reverse-engineering intent from every callsite.

The Solution: Type hints + mypy/pyright catch entire classes of bugs at edit time and double as living documentation. PEP 695 generics let library code be precise without verbosity.

Real Impact: Adoption is gradual — annotate public APIs first, then internals — and the payoff compounds the larger your codebase grows.

Real-World Analogy

Think of types as the labels on chemistry flasks:

  • Type hint = the label saying what's safe to mix in
  • mypy = the lab safety officer who checks labels before you mix
  • Generic[T] = a flask that holds any liquid but tells you what it has right now
  • Protocol = a label that says 'anything that pours' — duck typing made checkable
  • Any = an unlabeled flask — convenient and dangerous

Python is dynamically typed, but type hints + a checker (mypy, pyright) catch most bugs before runtime, improve IDE intelligence, and act as living documentation. Hints don't affect runtime — they're just metadata.

def greet(name: str, age: int = 0) -> str:
    return f"hello {name}"

greet(42)            # mypy: error — int is not str
greet("Alice")       # OK
$ pip install mypy
$ mypy main.py
main.py:5: error: Argument 1 to "greet" has incompatible type "int"; expected "str"

Built-in Types in Annotations

Since Python 3.9, use the built-in collection types directly as generics — no need for typing.List etc.

def total(prices: list[float]) -> float:
    return sum(prices)

def lookup(users: dict[int, str], uid: int) -> str | None:
    return users.get(uid)

def collect(items: set[str]) -> tuple[int, str]:
    return len(items), ", ".join(items)

Union and Optional

# Python 3.10+ — preferred
def parse(s: str) -> int | None: ...

# Older syntax — still valid
from typing import Union, Optional
def parse(s: str) -> Optional[int]: ...      # same as Union[int, None]

TypeVar and Generics

Write generic functions and classes that work with any type while preserving type information.

PEP 695 Syntax (Python 3.12+) — Recommended

def first[T](items: list[T]) -> T:
    return items[0]

first([1, 2, 3])          # inferred as int
first(["a", "b"])         # inferred as str

class Box[T]:
    def __init__(self, value: T):
        self.value = value

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

b: Box[int] = Box(42)
b.get()                    # mypy knows this is int

Classic Syntax (3.5-3.11)

from typing import TypeVar, Generic

T = TypeVar("T")

class Box(Generic[T]):
    def __init__(self, value: T):
        self.value = value

Bounded TypeVars

# T can only be a numeric type
def double[T: (int, float)](x: T) -> T:
    return x * 2

# Subclasses of a base type
def first_shape[T: Shape](items: list[T]) -> T:
    return items[0]

Callable, Final, and Special Types

from typing import Callable, Final, Any, Literal, TypedDict

# Callable — function-like values
def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
    return fn(a, b)

# Final — cannot be reassigned (enforced by mypy)
MAX_RETRIES: Final = 3

# Any — opt out of type checking (use sparingly)
def dynamic(x: Any) -> Any: ...

# Literal — exact value
def set_mode(mode: Literal["r", "w", "rb", "wb"]): ...

# TypedDict — structured dicts (great for JSON shapes)
class UserDict(TypedDict):
    name: str
    age: int
    email: str

user: UserDict = {"name": "A", "age": 30, "email": "[email protected]"}

Running mypy

(.venv) $ pip install mypy
(.venv) $ mypy main.py
(.venv) $ mypy --strict main.py    # max strictness

Configure in pyproject.toml

[tool.mypy]
python_version = "3.12"
strict = true
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false

Adopt gradually

Start with mypy at default strictness. Add hints to public APIs first. Once those are clean, dial up to --strict. Don't try to type a whole legacy codebase at once.

🎯 Practice Exercises

Exercise 1: Generic Stack

Implement Stack[T] using PEP 695 syntax. push(t: T), pop() -> T, __len__. Run mypy.

Exercise 2: Bounded function

Write sum_all[T: (int, float)](xs: list[T]) -> T. Verify mypy rejects sum_all(["a", "b"]).

Exercise 3: TypedDict JSON

Define a HttpResponse TypedDict mirroring a real JSON shape. Write a parser that returns it.

Exercise 4: Strict mode

Take a small untyped script. Run mypy --strict on it. Fix every warning until it passes.