Saying what a function expects, and what checks it
The docstring is the first line of the body
def split_bill(total, people, tip_percent=0):
"""Return the amount each person owes, rounded to 2 decimals.
total: the bill before tip, in rupees
people: how many are paying; must be at least 1
tip_percent: 10 means 10 percent
Raises ValueError if people is less than 1.
"""A string as the first statement of a function, module or class becomes its documentation. It is not a comment — it is stored on the object, so help(split_bill) prints it, editors show it on hover, and python -m pydoc yourmodule turns a file into readable documentation with no extra tooling.
Write the first line as a sentence saying what the function returns, in the imperative. Then the arguments in terms a caller needs — units, ranges, what "must" means. Then what it raises. Three-quarters of the value is in the units: total in rupees or paise is exactly the sort of thing that gets guessed wrong.
Do not restate the code. """Adds a and b.""" above def add(a, b) is noise. Document the parts a reader cannot see: units, edge cases, side effects, and why the odd-looking line is there.
Type hints
def split_bill(total: float, people: int, tip_percent: float = 0) -> float:
...The annotations after each colon and after the arrow say what types are expected. Common forms:
names: list[str]
scores: dict[str, int]
maybe: str | None # 3.10+; earlier, Optional[str]
pairs: tuple[int, int]
anything: AnyBefore Python 3.9 you needed from typing import List, Dict. In modern code, use the lowercase built-ins.
Here is the honest part: nothing enforces them
split_bill("lots", "several") # runs happily until the arithmetic failsPython does not check annotations at runtime. They are metadata. A function annotated -> int can return a string and nothing complains. Anyone who tells you type hints make Python type-safe is overselling them.
What they actually buy you:
- A checker you run yourself.
mypyandpyrightare free, read the annotations and report mismatches before you run anything.pip install mypy && mypy yourfile.pytakes a minute to set up and catches the class of bug where a function returnsNoneon one path and a number on another. - Editor help. Autocomplete and inline errors get dramatically better, because the editor knows what
peopleis. - Better AI assistance. An assistant reading annotated code guesses far less. This is a small, real, measurable benefit.
- Documentation that cannot drift as easily as a prose comment, because the checker complains when it stops matching.
The costs are real too. Hints on heavily dynamic code get long and ugly, and a codebase with hints on 40 per cent of its functions gives a false sense of coverage. The usual advice, and it is good advice: annotate the boundaries — the functions other modules call, and anything handling external data — and leave short internal helpers bare.
Runtime validation is a separate job
If you need the check to actually happen, do it:
if people < 1:
raise ValueError(f"people must be at least 1, got {people}")Or use pydantic, a free library that builds validation from annotations and is the standard way to parse and check JSON from an API. It is a different thing from mypy: mypy checks your code before it runs; pydantic checks your data while it runs. You often want both, for different reasons.
Comments earn their place differently
# The API returns paise for historical reasons; convert once, here.
amount = raw / 100Good comments explain why. The code already says what. A comment that repeats the line beneath it will eventually contradict it, because someone will change the line and not the comment, and then it is worse than nothing.
The one comment style always worth writing is the one recording a decision that looks wrong: the sleep that exists because the vendor rate-limits, the extra retry, the strange ordering. Without it, someone deletes the line and the bug returns in six months.
Try this now
Take the longest function you have written so far. Add a docstring giving units for every argument, annotate the signature, then run pip install mypy and mypy yourfile.py. Fix whatever it reports. The first run on real code usually finds something genuine.
The one thing to keep
Type hints are metadata that Python never enforces, so their value comes from a checker like mypy, from editor support, and from documenting units and edge cases the code cannot show.
Before you move on
A team adds `-> float` to every function and reports that the annotations are working because the code stopped crashing on bad input. What has most likely happened?
Pick the one you would defend. Nobody sees your answer.