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.

Try mypy Playground with a function accepting str | None. First call a string method without checking for None, then add the guard and compare the diagnostics. This exercises static narrowing; it does not execute or validate the input at runtime.

Source​

Explore connectionsOpen network