Hints#

Python is dynamically typed at runtime; type hints are an optional static layer. A type checker like mypy or pyright reads the hints and flags mismatches before the code runs. The interpreter itself does not enforce them.

Basics#

Annotate function parameters and the return type.

def add(a: int, b: int) -> int:
    return a + b

Annotate a variable at the binding site.

name: str = "operator"

Generic containers parameterise their element type.

counts: list[int] = []
index: dict[str, int] = {}
pair: tuple[int, str] = (1, "a")

Nullable values use X | None (Python 3.10+).

maybe: int | None = None

Pre-3.10 use Optional[int] or Union[int, None] from typing.

Built-in generics#

Since Python 3.9 the built-in collections accept [...] directly; no need to from typing import List.

Type

Meaning

list[T]

list whose elements are T

dict[K, V]

dict from K to V

tuple[T, U]

fixed-shape tuple

tuple[T, ...]

homogeneous variable-length tuple

set[T]

homogeneous set

frozenset[T]

homogeneous frozenset

Iterable[T]

any object that yields T (from collections.abc)

Iterator[T]

a one-shot iterator over T

Sequence[T]

indexable, length-bearing read-only sequence

Mapping[K, V]

read-only key-to-value mapping

Callable[[A, B], R]

callable with signature

Common patterns#

  • X | None — nullable value (a.k.a. Optional[X]).

  • X | Y — union type.

  • Any — escape hatch; turn off checking for this value.

  • Literal["a", "b"] — value restricted to a set of literals.

  • Final — prevent reassignment.

  • ClassVar — class attribute, not instance.

from typing import Literal, Final

Mode = Literal["read", "write", "append"]
MAX: Final = 1024

def open_log(path: str, mode: Mode = "read") -> None: ...

Structural types#

TypedDict, dict with named keys.

from typing import TypedDict, NotRequired

class User(TypedDict):
    id: int
    name: str
    email: NotRequired[str]

Protocol, structural typing (duck-typing checked statically).

from typing import Protocol

class HasName(Protocol):
    name: str

def greet(x: HasName) -> str: return f"hi {x.name}"

Anything with a name: str attribute satisfies HasName; no inheritance required.

Generics#

Python 3.12 PEP-695 syntax.

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

class Stack[T]:
    def __init__(self) -> None:
        self._xs: list[T] = []
    def push(self, x: T) -> None: self._xs.append(x)
    def pop(self) -> T: return self._xs.pop()

Pre-3.12 use TypeVar.

from typing import TypeVar
T = TypeVar("T")

def first(xs: list[T]) -> T: ...

Function types#

from typing import Callable, ParamSpec, Concatenate

handler: Callable[[Request], Response]

# ParamSpec for decorators that preserve the wrapped signature
P = ParamSpec("P")
def trace[T, **P](f: Callable[P, T]) -> Callable[P, T]: ...

Runtime introspection#

  • typing.get_type_hints(obj) — resolve string annotations.

  • typing.cast(T, value) — tell the checker “trust me”, no runtime effect.

  • typing.assert_type(x, T) — static assertion, no-op at runtime.

  • typing.NewType("UserId", int) — distinct nominal alias.

Tooling#

Tool

Use

mypy

Original; gradual; widely deployed.

pyright

Faster, stricter; default in VS Code (via Pylance).

pyre

Meta’s checker; powers Pysa security analysis.

ty

Strict checker from Astral (preview).

$ mypy src/
$ pyright

Run one in CI; treat type errors as build failures.

Pitfalls#

  • Type hints are not enforced at runtime. Validate inputs separately (pydantic is great for boundaries).

  • Any propagates. One Any infects callers.

  • Forward references ("Node") are needed for self-referential types, or use from __future__ import annotations.

  • Mutable default arguments (def f(x=[]):) still bite; the type checker may not catch it. Use None.

References#