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.