Type hints on functions & classes
Annotate functions with precise types while implementing a small module.
Types you write down, but Python won't enforce
A type hint is a note you attach to a name saying what kind of value belongs there. When you review a stranger's function, the signature def average(values: list[float]) -> float states the contract in one line: pass a list of floats, get a float back. Without hints you are reverse-engineering intent from the body. On real teams, hints plus a checker like mypy or pyright catch a whole class of bugs (passing a str where an int was meant) in CI, before the code ever runs.
A hint is metadata, not a check
Python records annotations but never acts on them at runtime. average(["a", "b"]) will happily start executing and only blow up inside sum (a TypeError from 0 + "a"), not at the call site. The payoff comes from tools that read the annotations: your editor's autocomplete, and static checkers run as a build step.
| When | Hints plus a checker in CI | Hints but no checker | No hints at all |
|---|---|---|---|
| As you type | the editor flags it immediately | the editor may flag it | nothing |
| In CI | the build fails before merge | passes | passes |
| At runtime | never reached | TypeError deep inside sum() | TypeError deep inside sum() |
def average(values: list[float]) -> float:
if not values:
return 0.0
return sum(values) / len(values)
values: list[float] annotates the parameter; -> float annotates the return. sum(values) / len(values) is always a float because / is true division, so the return hint is honest. (Passing [2, 4], which are int, still type-checks: a checker treats int as an acceptable float.)
Common shapes
name: str = "Ada"
counts: dict[str, int] = {}
pair: tuple[int, int] = (1, 2)
scores: list[float] = []
On classes
Annotate constructor parameters and the attributes they set. A @dataclass generates __init__ from the annotations for you.
class Account:
def __init__(self, balance: float) -> None:
self.balance: float = balance
-> None on __init__ is the convention: it returns nothing.
Across modules
Hints behave identically when code spans files. In the stats package, stats/summary.py can import a helper (from stats.rounding import round2) and annotate average exactly as it would in a single file. The annotation documents the boundary so a caller in another module knows the shape without opening the source.
Pitfalls
- Hints are not runtime validation. If you need to reject bad input at runtime, you still write an explicit check or reach for a validator like
pydantic.-> floatguarantees nothing on its own. roundon floats can surprise you. Two separate effects combine. First,roundbreaks exact ties to the nearest even digit, soround(2.5)is2, not3. Second, most decimals are not exactly representable:2.675is stored as2.67499..., soround(2.675, 2)returns2.67, not2.68, because the stored value is already below the tie. Expect small surprises when the Practiceround2helper trims a mean to 2 decimals.
Interview nuance: the interpreter ignores type hints at runtime. They are stored in a function's __annotations__ and read only by tools and libraries that opt in (checkers, editors, @dataclass, pydantic); the interpreter itself does zero type checking. So typed Python buys a build-time guarantee, not a runtime one, which is exactly why teams pair hints with mypy in CI rather than trusting them at the call site.
Sort each of these by whether it actually reads the type hints you wrote.
def average(values: list[float]) -> float:
if not values:
return 0.0
return sum(values) / len(values)
print(average([2, 4])) # 3.0
print(average([])) # 0.0Apply
Your turn
The task this lesson builds to.
Warm-up (one file): implement average(nums). Return the mean of nums rounded to 2
decimals, or 0.0 for an empty list.
For [10, 20, 35] return 21.67. Add type hints: def average(nums: list[float]) -> float.
3 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Implement average(values) in stats/summary.py: return 0.0 for an empty list, otherwise
the mean rounded with the read-only round2 helper imported from stats.rounding. Annotate it as
def average(values: list[float]) -> float. Some tests are hidden.
3 hints and 2 automated checks are waiting in the workspace.