Type hints and annotations
Model precise contracts with modern unions, generics, protocols, narrowing, and postponed annotation evaluation.
Annotations attach metadata to functions, classes, and variables. Type checkers use that metadata to find inconsistent calls without executing code. At runtime, Python ordinarily accepts values regardless of annotations:
def double(value: int) -> int:
return value * 2
assert double("ha") == "haha" # Runs, although a type checker should reject it.Use validation libraries or explicit checks at untrusted boundaries. Static typing and runtime validation solve different problems.
Precise everyday types
from collections.abc import Iterable, Mapping
def summarize(
values: Iterable[float],
labels: Mapping[str, str] | None = None,
) -> tuple[int, float]:
materialized = tuple(values)
return len(materialized), sum(materialized)Use abstract input types when only abstract behavior is required and concrete return types when callers receive a specific object. Modern syntax includes:
list[str]rather thantyping.List[str].User | Nonerather thanOptional[User].collections.abc.Callableand collection interfaces.Literalfor a closed set of literal values.TypedDictfor dictionaries with a known key shape.
Avoid annotating everything as Any. Any permits every operation and allows
the resulting uncertainty to spread. Use object when a value can be anything
but must be inspected or narrowed before use.
Narrowing unions
def normalize_identifier(value: int | str) -> str:
if isinstance(value, int):
return str(value)
return value.strip().lower()The branch narrows value for a type checker. Other narrowing tools include
is None, pattern matching, TypeGuard, and assertion functions. Do not cast
merely to silence an error: typing.cast changes the checker's view but performs
no runtime conversion or validation.
from typing import TypeGuard
def is_string_list(values: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(value, str) for value in values)Generics preserve relationships
A generic says that types at different positions are related:
from collections.abc import Iterable
from typing import TypeVar
T = TypeVar("T")
def first(values: Iterable[T]) -> T:
iterator = iter(values)
try:
return next(iterator)
except StopIteration as error:
raise ValueError("expected at least one value") from errorPassing Iterable[Customer] produces a Customer; passing Iterable[str]
produces a str. Returning object would lose this relationship, while Any
would hide mistakes after the call.
Python 3.12 introduced type-parameter syntax:
def identity[T](value: T) -> T:
return value
class Box[T]:
def __init__(self, value: T) -> None:
self.value = valueUse this syntax only when the project's minimum Python version and type checker
support it. TypeVar remains appropriate for libraries supporting older Python.
Variance in one practical example
If Cat is an Animal, a producer of cats can safely serve where a producer of
animals is expected, but a mutable list[Cat] cannot serve as list[Animal].
The latter would let a caller append a Dog to a list promised to contain cats.
Read-only interfaces such as Sequence are often covariant; mutable containers
are generally invariant. Model the operations consumers need instead of forcing
concrete mutable collections through unsafe casts.
Callable signatures
from collections.abc import Callable
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def invoke(
function: Callable[P, R],
*args: P.args,
**kwargs: P.kwargs,
) -> R:
return function(*args, **kwargs)ParamSpec captures a complete parameter list. It is especially valuable for
decorators. Callable[..., R] preserves only the return type and allows any
arguments, so use it only when the parameters are genuinely unknown.
Protocols and typed mappings
Use a protocol for object behavior and TypedDict for a dictionary schema:
from typing import Protocol, TypedDict
class UserPayload(TypedDict):
id: int
display_name: str
class UserSource(Protocol):
def fetch(self, user_id: int) -> UserPayload | None: ...TypedDict values remain ordinary dictionaries at runtime. If construction,
validation, methods, or runtime identity matter, use a data class or validation
model instead.
from __future__ import annotations
Historically, annotations were evaluated as function and class definitions ran, which made forward references and import cycles awkward. This future import stores annotations in a postponed form:
from __future__ import annotations
class Node:
def __init__(self, children: list[Node] | None = None) -> None:
self.children = children or []Without postponed evaluation on affected Python versions, Node would need to
be quoted because its class body has not finished. The future import must appear
near the top of the module.
Do not assume __annotations__ contains ready-to-use classes. Use
typing.get_type_hints() when runtime code must resolve annotations, and be
aware that resolving annotations may require namespaces and can execute
annotation-related evaluation. Avoid using annotation evaluation on untrusted
definitions.
Python's annotation behavior continues to evolve by version. Libraries that inspect annotations must test against every supported interpreter rather than depending on raw storage details.
Tradeoffs and pitfalls
- Precise types improve refactoring but overly complex types can obscure design.
# type: ignoreshould include a specific error code and an explanation.FinalandClassVarcommunicate intent; they are not runtime enforcement.NewTypedistinguishes logically different values statically with almost no runtime cost, but does not perform validation.@overloaddescribes multiple call signatures; one runtime implementation is still required.- Type hints for third-party data do not make that data valid.
Practice
- Replace an
Anyreturn with a generic that preserves the input element type. - Define a protocol for a cache with typed
getandputbehavior. - Model a JSON object first with
TypedDict, then with a data class. Explain which boundary each suits. - Use
get_type_hints()on a class with a forward reference and compare its result with raw__annotations__.
LEARNING RECORD
Finish this lesson
Mark it complete when you can explain the main decision without looking.
KNOWLEDGE CHECK
Type hints and annotations
Decorators and closures
Capture lexical state, build correct wrappers, preserve metadata, and choose explicit alternatives when wrapping hides too much.
Exceptions and context managers
Design explicit failure boundaries, preserve exception context, aggregate concurrent errors, and guarantee resource cleanup.