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