Skip to main content

Type Hints and Docstrings

What You'll Learn​

How to write docstrings that explain what code does, and type hints that describe what data it expects and returns.

Why Document Code?​

Without documentation:

  • You'll forget what a function does in 2 weeks
  • Teammates waste time reverse-engineering intent
  • IDEs can't give useful auto-complete
  • Type checkers can't find bugs

With documentation:

  • help(function) shows useful information
  • IDEs show parameter types while you type
  • mypy finds type errors before runtime
  • Bugs are caught earlier, reviews are faster

Docstrings​

A docstring is a string literal at the start of a module, class, or function. Python stores it in __doc__:

def calculate_bmi(weight_kg: float, height_m: float) -> float:
"""
Calculate Body Mass Index (BMI).

Args:
weight_kg: Body weight in kilograms (must be positive).
height_m: Height in metres (must be positive, non-zero).

Returns:
BMI value as a float. Normal range: 18.5–24.9.

Raises:
ValueError: If height_m is zero or negative.

Example:
>>> calculate_bmi(70, 1.75)
22.857142857142858
"""
if height_m <= 0:
raise ValueError(f"height_m must be positive, got {height_m}")
return weight_kg / (height_m ** 2)

Access docstrings:

help(calculate_bmi)
print(calculate_bmi.__doc__)

One-Line Docstrings​

For simple functions, one line is enough:

def double(n: int) -> int:
"""Return n multiplied by 2."""
return n * 2

def is_even(n: int) -> bool:
"""Return True if n is even."""
return n % 2 == 0

Class Docstrings​

from dataclasses import dataclass

@dataclass
class Product:
"""
Represents a product in the catalog.

Attributes:
name: Product display name.
price: Price in USD (must be >= 0).
in_stock: Whether the product is available.
"""
name: str
price: float
in_stock: bool = True

def apply_discount(self, rate: float) -> "Product":
"""
Return a new Product with a discounted price.

Args:
rate: Discount rate between 0.0 and 1.0.

Returns:
New Product with reduced price.

Raises:
ValueError: If rate is outside 0.0–1.0 range.
"""
if not 0.0 <= rate <= 1.0:
raise ValueError(f"rate must be 0.0–1.0, got {rate}")
return Product(self.name, self.price * (1 - rate), self.in_stock)

Type Hints — Complete Reference​

Primitives​

x: int = 5
y: float = 3.14
name: str = "Alice"
active: bool = True
nothing: None = None

Collections (Python 3.9+ — use built-in types)​

items: list[str] = ["a", "b", "c"]
pair: tuple[int, int] = (1, 2)
any_tuple: tuple[int, ...] = (1, 2, 3, 4) # variable-length tuple
tags: set[str] = {"x", "y"}
config: dict[str, int] = {"port": 5432}

Optional and Union​

# Python 3.10+ (preferred)
def find(id: int) -> str | None:
...

# Python 3.9 and earlier
from typing import Optional
def find(id: int) -> Optional[str]:
...

# Multiple types
from typing import Union
value: int | float | str # Python 3.10+
value: Union[int, float] # older syntax

Callable​

from typing import Callable

# A function that takes (int, str) and returns bool
handler: Callable[[int, str], bool]

# A function with no args that returns None
callback: Callable[[], None]

Generic Collections​

from typing import Iterator, Generator, Sequence, Mapping

def get_names() -> Iterator[str]:
yield "Alice"
yield "Bob"

def process(items: Sequence[int]) -> None: # accepts list, tuple, etc.
...

def lookup(data: Mapping[str, int]) -> None: # accepts dict, etc.
...

TypeAlias — Name Complex Types​

from typing import TypeAlias

# Instead of repeating complex types
UserRecord: TypeAlias = dict[str, str | int | list[str]]

def load_user(user_id: int) -> UserRecord:
...

Type Checking with mypy​

mypy checks type hints without running your code:

pip install mypy
mypy src/main.py
mypy src/ # check entire directory

Example: catching a bug before it crashes:

def greet(name: str) -> str:
return "Hello, " + name

greet(42) # mypy: error: Argument 1 has incompatible type "int"; expected "str"

Doctest — Examples That Double as Tests​

def add(a: int, b: int) -> int:
"""
Add two integers.

>>> add(2, 3)
5
>>> add(-1, 1)
0
>>> add(0, 0)
0
"""
return a + b

Run doctests:

python3 -m doctest module.py -v

Documentation Styles​

Choose one and be consistent:

Google style (most common in Python):

def fn(x: int, y: str) -> bool:
"""Short description.

Args:
x: Description of x.
y: Description of y.

Returns:
True if successful.

Raises:
ValueError: If x is negative.
"""

NumPy style (common in scientific code):

def fn(x, y):
"""
Short description.

Parameters
----------
x : int
Description of x.
y : str
Description of y.

Returns
-------
bool
True if successful.
"""

What to Always Document​

Always documentOptional
Public functions/methodsPrivate helpers (if obvious)
Non-obvious parametersTrivial one-liners
What exceptions are raisedInternal variables
Return value meaningImplementation details

Quick Reference​

# One-line docstring
def fn():
"""Return the result of calculation."""

# Full docstring
def fn(x: int) -> str:
"""
Short summary.

Args:
x: What x is.

Returns:
What the function returns.

Raises:
ValueError: When and why.

Example:
>>> fn(5)
'five'
"""

# Type hints
param: type
param: type | None
param: list[str]
param: dict[str, int]
-> return_type

# Run type checker
# mypy src/

# Run doctests
# python3 -m doctest module.py

What's Next​

→ Lesson 5: Project Architecture Patterns