Skip to main content

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, or Mapping when the function does not require a concrete container.
  • Return a concrete type when callers depend on its behavior.
  • Use Literal or an enum for a small closed set of meaningful values.
  • Use TypedDict for dictionary-shaped records and dataclasses or ordinary classes when values have behavior and invariants.
  • Use Protocol for structural interfaces; inheritance is not required.
  • Keep Any at untyped boundaries and narrow it quickly. Any disables 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.

Source