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 whose elements are |
|
dict from |
|
fixed-shape tuple |
|
homogeneous variable-length tuple |
|
homogeneous set |
|
homogeneous frozenset |
|
any object that yields |
|
a one-shot iterator over |
|
indexable, length-bearing read-only sequence |
|
read-only key-to-value mapping |
|
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 |
|---|---|
|
Original; gradual; widely deployed. |
|
Faster, stricter; default in VS Code (via Pylance). |
|
Meta’s checker; powers Pysa security analysis. |
|
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 (
pydanticis great for boundaries).Anypropagates. OneAnyinfects callers.Forward references (
"Node") are needed for self-referential types, or usefrom __future__ import annotations.Mutable default arguments (
def f(x=[]):) still bite; the type checker may not catch it. UseNone.