Type Hints
Type hints let you write down what types a function expects and returns. Python itself doesn't enforce them at runtime, but editors use them for autocomplete and error highlighting, and tools like mypy and Pyright check them before your code runs — catching bugs early and documenting your code at the same time.
Annotating functions and variables
def greet(name: str, times: int = 1) -> str: return ", ".join([f"Hello {name}"] * times) count: int = 0 ratio: float = 0.5 names: list[str] = ["Ana", "Ben"] print(greet("Ana", 2)) print(greet.__annotations__)
Hello Ana, Hello Ana
{'name': <class 'str'>, 'times': <class 'int'>, 'return': <class 'str'>}Parameters get : type, the return type goes after ->. Hints are stored on the function but otherwise ignored when the code runs.
Hints are not enforced at runtime
def double(n: int) -> int: return n * 2 print(double("ha"))
haha
Python happily runs this. A type checker, however, would report: Argument of type "str" cannot be assigned to parameter "n" of type "int". That's the point of hints — find the mistake before running the code.
Collection types
def average(scores: list[float]) -> float: return sum(scores) / len(scores) def word_counts(text: str) -> dict[str, int]: counts: dict[str, int] = {} for word in text.split(): counts[word] = counts.get(word, 0) + 1 return counts point: tuple[int, int] = (3, 4) row: tuple[str, ...] = ("a", "b", "c") tags: set[str] = {"python"} print(average([7.5, 9.0]), word_counts("a b a"))
8.25 {'a': 2, 'b': 1}Built-in collection types take their item types in square brackets. tuple[int, int] is a pair; tuple[str, ...] is any length.
Optional values and unions
def find_user(user_id: int) -> str | None: users = {1: "Ana", 2: "Ben"} return users.get(user_id) def to_text(value: int | float | str) -> str: return str(value) user = find_user(3) if user is not None: print(user.upper()) else: print("not found") print(to_text(4.5))
not found 4.5
X | Y means "either type". str | None is the standard way to say a value might be missing — a type checker will then insist you check for None before calling string methods, preventing a classic crash.
Callables, Any and Literal
from collections.abc import Callable, Iterable from typing import Any, Literal def apply(fn: Callable[[int], int], values: Iterable[int]) -> list[int]: return [fn(v) for v in values] def log(level: Literal["info", "warning", "error"], message: str) -> None: print(f"[{level.upper()}] {message}") def debug(value: Any) -> None: print(repr(value)) print(apply(lambda n: n * 10, range(3))) log("warning", "disk almost full") debug({"x": 1})
[0, 10, 20]
[WARNING] disk almost full
{'x': 1}Callable[[int], int]— a function taking an int and returning an int.Iterable[int]— anything you can loop over; accepting abstract types makes functions more flexible.Literal[...]— only these exact values; a checker flagslog("warn", …).Any— opt out of checking for that value. Use sparingly.
Type aliases and generics
type Point = tuple[float, float] type Matrix = list[list[float]] def distance(a: Point, b: Point) -> float: return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5 def first[T](items: list[T]) -> T | None: return items[0] if items else None print(distance((0, 0), (3, 4))) print(first(["x", "y"]), first([]))
5.0 x None
The type statement (Python 3.12+) names a complex type. first[T] declares a generic function: whatever type of list goes in, the checker knows that same type comes out.
Typing classes and records
from typing import TypedDict, NotRequired class Movie(TypedDict): title: str year: int rating: NotRequired[float] def describe(m: Movie) -> str: return f"{m['title']} ({m['year']})" print(describe({"title": "Arrival", "year": 2016}))
Arrival (2016)
TypedDict describes the shape of a dictionary, such as parsed JSON. For your own data structures, dataclasses (lesson 40) combine type hints with automatic methods.
Running a type checker
pip install mypy mypy app.py
app.py:4: error: Argument 1 to "double" has incompatible type "str"; expected "int" [arg-type] Found 1 error in 1 file (checked 1 source file)
Popular checkers are mypy, Pyright (which powers VS Code's Pylance extension) and newer fast tools such as ty and Pyrefly. Many editors run one automatically, underlining mistakes as you type.
How much to annotate
A practical approach: annotate function signatures (parameters and return types) everywhere, and let the checker infer local variables. Add hints gradually to existing code, starting with the most-used functions — you don't need to annotate everything at once.