Docstrings and type hints
A function that works is not finished. Somebody — usually you, months later — has to work out what it takes, what it gives back, and whether it is safe to call. Docstrings and type hints answer that without them having to read the body.
Docstrings
A string on the first line of a function is its documentation:
def apply_gst(amount):
"""Return the amount with 18% GST added."""
return amount + (amount * 0.18)
Triple quotes by convention, even for one line. Python stores it:
print(apply_gst.__doc__)
help(apply_gst)
help() is the same function that described built-ins back in the REPL lesson.
Write a docstring and your own functions appear there too. Your editor will show
it on hover, which is where it earns its keep daily.
For anything with more than one parameter, the fuller form:
def calculate_total(subtotal, tax_rate=0.18, discount=0):
"""Work out an order total.
Args:
subtotal: The order value before tax and discount.
tax_rate: Tax as a decimal. Defaults to 0.18 (18% GST).
discount: Flat amount off, applied before tax.
Returns:
The final total, rounded to two decimal places.
Raises:
ValueError: If the discount exceeds the subtotal.
"""
if discount > subtotal:
raise ValueError("Discount cannot exceed subtotal")
taxable = subtotal - discount
return round(taxable + (taxable * tax_rate), 2)
Summary line, blank line, details. This is Google style; NumPy style is another common one. Pick whichever your team uses and be consistent — consistency matters more than which.
What to write
Say why, or what is not obvious. Do not restate the code.
def get_name(user):
"""Get the name.""" # useless
def get_name(user):
"""Return the user's display name, falling back to their email.
Accounts created through the CSV import have no name set.
"""
The second tells a reader something they could not get from the signature — and the second paragraph explains why the fallback exists, which is exactly the kind of thing that gets deleted by somebody who does not know.
Document surprises. If a function modifies its argument, changes something elsewhere, or is slow, say so. That is the information a caller cannot see.
Short, obvious functions need no docstring. def is_even(n): return n % 2 == 0
is already clear, and a docstring on it is noise.
Type hints
Python does not check types at runtime. Hints tell readers and tools what you expect:
def apply_gst(amount: float) -> float:
return amount + (amount * 0.18)
amount: float is the parameter. -> float is the return. Both are optional
and ignored while the program runs:
print(apply_gst("hello"))
TypeError: can't multiply sequence by non-int of type 'float'
The error came from the multiplication, not the hint. Hints do not enforce anything. What they do:
- Your editor autocompletes and warns you as you type.
- A checker like
mypyfinds mistakes without running the code. - A reader knows what is expected without reading the body.
That last one is the reason to write them even alone on a small project.
The types you will use
def greet(name: str) -> str: ...
def add(a: int, b: int) -> int: ...
def is_valid(email: str) -> bool: ...
def save(data: str) -> None: ...
-> None for a function that returns nothing. Say it explicitly — it tells a
reader not to expect a value.
Collections say what is inside them:
def total(prices: list[float]) -> float: ...
def lookup(prices: dict[str, int]) -> int: ...
def stats(numbers: list[int]) -> tuple[int, int, float]: ...
list[float] is far more useful than list. It is the difference between
knowing you got a list and knowing what is in it.
When a value might be missing:
def find_user(users: list[dict], email: str) -> dict | None:
...
dict | None means "a dictionary or None". That single hint tells every
caller they must handle the not-found case — which is exactly the check people
forget. On Python 3.9 and earlier you would write Optional[dict] from
typing; the | form is 3.10 and up and is what you should write now.
Defaults and hints together:
def greet(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}!"
Note the spaces around = once a hint is present. Without a hint it is
greeting="Hello", no spaces. That is PEP 8, and your formatter will do it for
you.
Both together
def find_user(users: list[dict], email: str) -> dict | None:
"""Find a user by email address.
Args:
users: The users to search.
email: The address to match, compared case-insensitively.
Returns:
The matching user, or None if there is no match.
"""
target = email.strip().lower()
for user in users:
if user["email"].lower() == target:
return user
return None
You can call that correctly without reading a single line of the body. That is the whole goal.
How much is enough
Hints and docstrings are not free, and over-applied they become clutter. A reasonable standard:
- Type hints on everything you would expect somebody else to call. They cost a few characters and never go stale silently, because a checker catches it.
- Docstrings on anything whose purpose is not obvious from its name and signature, and on anything with a surprise in it.
- Neither on a three-line private helper whose name already says it.
The failure mode to avoid is a docstring that has drifted out of date. A wrong comment is worse than none, because it is believed. If you change what a function does, change its docstring in the same edit.
Checking your hints
pip install mypy
mypy your_file.py
Given apply_gst(amount: float) -> float, calling apply_gst("hello") is
reported before you run anything:
error: Argument 1 to "apply_gst" has incompatible type "str"; expected "float"
Worth trying once so you know what the tooling gives you. On a real project it is the difference between hints as documentation and hints as a safety net.
Practice
- Add a one-line docstring to a function you wrote earlier. Call
help()on it. - Write
calculate_totalwith the full Args/Returns/Raises docstring. - Write a deliberately useless docstring, then rewrite it to say something the signature does not.
- Add type hints to
add,greet,is_evenandsave. - Hint a function taking a list of integers and returning a tuple of three values.
- Write
find_userreturningdict | Noneand explain what that tells a caller. - Install
mypy, run it on a file where you call a function with the wrong type, and read the output. - Go back to your guessing game and add hints and docstrings to any functions in it. If it has none yet, the next lesson fixes that.
Next: taking that guessing game apart and putting it back together properly.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship