Python type hints, explained
Type hints let you annotate what types your functions and variables expect — improving editor autocomplete, catching bugs before runtime, and documenting intent. The catch that interviewers love: Python does not enforce them at runtime. Understanding what hints do and don't do is the heart of this topic. This guide covers the syntax and the tooling.
Hints are not enforced at runtime
This is the single most important fact: type hints are annotations, not runtime checks. The interpreter records them but does not validate them — passing the "wrong" type runs fine unless your own code breaks.
def greet(name: str) -> str:
return "hi " + name
greet(42) # TypeError — but from the +, NOT from the hint
# Python never checked that 42 is a str
Hints exist for humans and tools: editors use them for autocomplete and warnings, and static checkers like mypy verify them before you run. Enforcement is opt-in via those external tools (or libraries like Pydantic), never automatic.
Basic annotations
Annotate parameters, return types, and variables with name: Type. The return type goes
after ->.
def add(a: int, b: int) -> int:
return a + b
count: int = 0
names: list[str] = []
ratio: float = 1.5
These are visible at runtime in __annotations__, which is how tools like dataclasses and
Pydantic read them — but again, nothing is checked automatically.
Optional and Union (and the | syntax)
Optional[X] means "X or None" — common for default-None arguments. Union[X, Y]
means "either type." Since Python 3.10 you can write X | None and X | Y directly.
from typing import Optional, Union
def find(id: int) -> Optional[str]: # may return a str or None
...
def parse(x: Union[int, str]) -> int: # accepts int or str
...
# 3.10+ shorthand:
def find(id: int) -> str | None: ...
def parse(x: int | str) -> int: ...
Optional[X] is exactly Union[X, None] — it does not mean "optional argument," just
"could be None." A checker will then force you to handle the None case before using the
value.
Built-in generics: listint, dictstr, int
Since Python 3.9 you annotate containers with the built-in types directly —
list[int], dict[str, int], tuple[int, ...] — instead of importing List, Dict from
typing (which are now deprecated aliases).
def totals(scores: dict[str, list[int]]) -> dict[str, int]:
return {name: sum(vals) for name, vals in scores.items()}
The lowercase built-in forms are the modern style; the capitalised typing.List/Dict are
only needed on Python 3.8 and earlier.
Any vs object
Any is an escape hatch: it's compatible with everything in both directions, so the
checker stops checking — use it sparingly. object is the actual base of all types, so you
can assign anything to it but can only do object-level operations without a cast.
from typing import Any
x: Any = "anything"
x.foo().bar() # checker says nothing — Any disables checking
y: object = "hi"
y.upper() # checker ERROR — object has no .upper(); narrow it first
Rule of thumb: Any means "trust me, stop checking"; object means "literally any object,
but stay type-safe." Prefer object (or a precise type) and reserve Any for genuinely
dynamic boundaries.
What mypy does
mypy (and similar checkers like Pyright) is a static analysis tool: it reads your
hints and flags type mismatches without running the code. This is where hints turn into
actual bug-catching.
def double(n: int) -> int:
return n * 2
double("oops") # mypy: error: Argument 1 to "double" has incompatible type "str"
mypy yourmodule.py # run the checker; it never executes your program
Add it to CI and you catch a whole class of None-handling and wrong-type bugs before they
reach production — the real payoff of annotating your code.
Recap
Type hints are annotations, not runtime checks — Python records but never enforces
them; the value comes from editors and static checkers like mypy. Annotate with
name: Type and -> ReturnType, use Optional[X]/X | None for "maybe None" and
Union/X | Y for alternatives, and prefer built-in generics (list[int],
dict[str, int]) on modern Python. Treat Any as an escape hatch that disables checking
and object as the safe "anything" type. Hints don't change how your program runs — they
make bugs visible before it runs.