Practical Type Hints in Python
Type hints document intended contracts and support static analysis, editors, and refactoring. Python does not enforce function or variable annotations at runtime. Validate untrusted input separately.
from collections.abc import Iterable
def average(values: Iterable[float]) -> float:
numbers = list(values)
if not numbers:
raise ValueError("values must not be empty")
return sum(numbers) / len(numbers)
Annotate public boundaries and important internal models first. Local variables that a checker can infer rarely need annotations.
Target a Python version
Typing syntax evolves with Python. Declare the minimum supported version in the
project, configure the checker to match it, and use typing_extensions when a
newer typing feature must be backported.
For Python 3.10 and later, prefer built-in generics and | unions:
def lookup(names: list[str], fallback: str | None = None) -> str: ...
The type statement and square-bracket generic parameter syntax require Python
3.12 or later. Use older TypeAlias and TypeVar forms when the runtime target
requires them.
Model meaning, not storage trivia
- Accept abstract capabilities such as
Iterable,Sequence, orMappingwhen the function does not require a concrete container. - Return a concrete type when callers depend on its behavior.
- Use
Literalor an enum for a small closed set of meaningful values. - Use
TypedDictfor dictionary-shaped records and dataclasses or ordinary classes when values have behavior and invariants. - Use
Protocolfor structural interfaces; inheritance is not required. - Keep
Anyat untyped boundaries and narrow it quickly.Anydisables useful checking rather than meaning “unknown but safe.”
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None: ...
def finish(resource: SupportsClose) -> None:
resource.close()
Narrowing and absence
Handle unions through real runtime evidence:
def length(value: str | bytes | None) -> int:
if value is None:
return 0
if isinstance(value, bytes):
return len(value)
return len(value)
Optional[T] means T | None; it does not mean the function parameter has a
default. Avoid cast as a substitute for checking—the call changes only the
static checker's view.
Runtime boundary
Annotations may be represented or evaluated differently across supported Python
versions. Code that introspects them should use documented APIs such as
typing.get_type_hints and must account for imports, forward references, and
possible evaluation effects.
A useful typing workflow runs one configured checker in CI, treats suppressions as narrow documented exceptions, and tests runtime behavior independently. Checker agreement is not proof of correctness; it is evidence that one class of contract mismatch was not found.