Home Tutorials Modern Python

Modern Python

Type Hints in Python: Annotate Code Without Changing Its Behaviour

Pyford Notes July 1, 2026 8 min read
Key points
  • Type hints are optional annotations; Python ignores them at runtime.
  • Static analysis tools like mypy and IDE checkers use them to catch errors before execution.
  • Use X | None (Python 3.10+) or Optional[X] for values that may be absent.
  • Built-in types like list, dict, and tuple are directly usable as generic aliases since Python 3.9.

Basic variable annotations

A variable annotation uses a colon after the name. The annotation is an expression, typically a type, but Python does not enforce it at runtime:

count: int = 0
name: str = "unnamed"
ratio: float
active: bool = True

An annotated variable without a value (ratio: float) declares a name and its expected type but does not create the variable in the usual sense—accessing it before assignment still raises NameError.

Annotating function signatures

Parameters get a colon annotation; the return type uses an arrow ->. Both are part of the function's __annotations__ dict but have no runtime effect on how the function behaves:

def greet(name: str, repeat: int = 1) -> str:
    return (f"Hello, {name}! " * repeat).strip()

def save_user(user_id: int, data: dict) -> None:
    ...   # None means no return value

def transform(values: list[float]) -> list[float]:
    return [v * 1.1 for v in values]

The benefit shows up in your editor and in mypy: calling greet(42) will trigger a type error because 42 is int, not str, even though Python would happily run it.

Optional values and None

When a value might be None, express it explicitly. Python 3.10 introduced the union operator | for this:

# Python 3.10+ syntax
def find_user(user_id: int) -> dict | None:
    ...

# Equivalent older syntax using Optional from typing
from typing import Optional
def find_user(user_id: int) -> Optional[dict]:
    ...

Declaring the possibility of None in the return type forces callers to handle it. A type checker will flag code that accesses the returned dict without first checking for None.

Union types

Use | when a parameter or return value can be one of several types:

def process(value: int | float | str) -> str:
    return str(value).strip()

# Older typing module equivalent:
from typing import Union
def process(value: Union[int, float, str]) -> str:
    ...

Unions are most useful in interfaces that accept multiple input forms, for example a function that takes either a file path as a string or a Path object.

Generics: typed containers

Since Python 3.9, built-in collection types accept type parameters directly:

def top_n(scores: dict[str, int], n: int) -> list[str]:
    return sorted(scores, key=scores.get, reverse=True)[:n]

Matrix = list[list[float]]

def flatten(matrix: Matrix) -> list[float]:
    return [cell for row in matrix for cell in row]

For older code bases (Python 3.8), import the capitalised versions from typing: List, Dict, Tuple, Set.

Annotated vs unannotated: a comparison

Without hintsWith hintsBenefit
def calc(x, y): def calc(x: float, y: float) -> float: IDE shows parameter types on hover
result = find(id) result: User | None = find(id) Checker warns if None not handled
items = [] items: list[str] = [] Appending non-string triggers warning
config = {} config: dict[str, Any] = {} Documents expected shape of the dict

Type hints at runtime

Python stores annotations in __annotations__ but does not evaluate or enforce them during execution. You can read them programmatically:

def greet(name: str) -> str:
    return f"Hello, {name}"

print(greet.__annotations__)
# {'name': , 'return': }

Libraries like dataclasses and pydantic do read annotations at runtime to generate behaviour—but that is a feature of those libraries, not of Python's type system itself. A plain function's annotations are purely informational unless something explicitly reads __annotations__.

Start annotating function signatures in new code. You do not need to annotate every local variable; focus on the boundaries where types matter most: public API parameters, return values, and data structure shapes.