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

PY basics.py
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__)
Output
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

PY not_enforced.py
def double(n: int) -> int:
    return n * 2

print(double("ha"))
Output
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

PY collections_hints.py
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"))
Output
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

PY unions.py
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))
Output
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

PY more_types.py
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})
Output
[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 flags log("warn", …).
  • Any — opt out of checking for that value. Use sparingly.

Type aliases and generics

PY generics.py
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([]))
Output
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

PY typed_dict.py
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}))
Output
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

$ terminal
pip install mypy
mypy app.py
Output
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.

Try it yourself

Try it yourself
Console output