Grade by Grade
C. Designing Modular Programs · Modular and Object-Oriented Design

Documenting Modules, Classes and Interfaces

A4.3 C1.4About 70 minutes
Learning goals
  • I can document a package and its modules so a programmer can find the right code without reading it all.
  • I can write class docstrings that state what the class represents, its attributes and its invariant.
  • I can document an interface: the contract on an abstract method, what an override changes, and what subclasses must do.
  • I can mark a module's public API with __all__ and underscores, and document public and internal code to different depths.
  • I can use help(), inspect.getdoc and doctest to read and check documentation across a multi-file program.

Words to know

API (application programming interface)
The public functions, classes and methods a module offers to other code, with their documented behaviour.
`__all__`
A module-level list of the names that make up the module's public API; from module import followed by an asterisk imports only these.
class docstring
The docstring right under a class line: what the class represents, its main attributes and its invariant.
contract
The documented promises of a method: what callers must provide (preconditions) and what it guarantees in return (postconditions).
subclass contract
What every subclass must do, usually written on the abstract methods and in the superclass docstring.
deprecation
Marking a feature as still working but scheduled to be removed, and documenting what to use instead.
inspect.getdoc
A standard-library function that returns a cleaned-up docstring, falling back to the inherited one when a method has none.

Documentation that scales

A 50-line script can survive with a few comments. A program of ten modules and twenty classes cannot: a new teammate needs to know which module to open, which classes they may use, what each class promises, and what a subclass must do. Expectation A4.3 asks for fully documented code to industry standards. In a modular program, that means documentation at every level: package, module, class, method and interface.

Remember from Data Types, Files and Expressions

In Code Maintenance: Documenting Code to Industry Standards you learned PEP 8 naming, PEP 257 docstrings with a summary line, the Google-style Args / Returns / Raises sections, type hints, block and line comments that explain why, doctests, and how Java doc comments compare. This lesson applies those tools to the larger pieces of this unit: packages, modules, classes and class hierarchies.

people who USE the programuser manual, help screens, tutorialsnext lessonprogrammers who CALL your codedocstrings, help(), type hintsthis lessonprogrammers who CHANGE your codeblock and line comments, clear namesthis lessontoolsdoctest, type checkers, documentation generatorsread the same docstrings and hints
Different readers need different documentation. Industry standards (PEP 8 and PEP 257 for Python) mostly cover the middle two rows.

Package and module docstrings: maps

  • A package docstring (in __init__.py) is a map: one line per module saying what it is for. The arena package in the final lesson of this unit opens this way.
  • A module docstring says what the module is for, the units and conventions it uses (such as "hours on a 24-hour clock"), and its public API.
  • List the public names in __all__, a list of strings at the top of the module. It tells readers and tools which names are the API; anything else, especially names starting with an underscore, is internal and may change.

A reader should be able to choose the right module from the package docstring alone, and the right function or class from the module docstring alone.

package arena/__init__.pya map: what each module is formodule arena/models.pypurpose, public API (__all__), units, constantsclass Bookingwhat it represents, attributes, invariantpublic method cost()summary, Args, Returns, Raises, side effectsinternal helper _start_of()one-line docstring or a comment is enough
Documentation at every level of a modular program. The higher the level, the more readers depend on it, so public modules, classes and methods get the most complete docstrings.

Class docstrings: what an object promises

Put the class docstring directly under the class line. It should say:

  • what one object represents ("One reservation of the ice surface");
  • its important attributes and their units, in an Attributes: section;
  • its invariant, the rule it always keeps ("start < end, both within opening hours");
  • for an abstract class, the subclass contract: which methods subclasses must override and what those methods must guarantee.

Document the constructor's arguments either in the class docstring or in __init__'s docstring; pick one place per project. Document each property with a docstring on its getter, because help() shows it as an attribute.

Worked example

Example 1: Documenting a module with a class

Document an arena.py module for booking ice time: a helper overlaps, and a Booking class with an invariant. Include examples that doctest can check, and a self-test.

  1. Module docstring: what the module offers and its conventions (whole hours on a 24-hour clock). A new reader knows the context in four lines.
  2. Named constants with line comments: OPEN_HOUR = 6 # the arena opens at 6 a.m.
  3. overlaps has a full function docstring with the rule that matters most (start included, end excluded) and two doctest examples, one of which tests that boundary.
  4. Booking has a class docstring with an Attributes: section. __init__ documents the ValueError that protects the invariant. Type hints record the expected types; other: "Booking" is in quotes because the class is still being defined.
  5. The self-test runs doctest.testmod(). TestResults(failed=0, attempted=2) means both documented examples are still true. overlaps.__doc__.splitlines()[0] reads the summary line from inside the program.

The module and its self-test output are below. Most of the file is documentation, which is normal for code that other modules import.

"""Tools for booking ice time at a community arena.
This module provides the Booking class and the overlaps() helper.
Times are whole hours on a 24-hour clock (for example, 19 means 7 p.m.).
"""
OPEN_HOUR = 6 # the arena opens at 6 a.m.
CLOSE_HOUR = 23 # and closes at 11 p.m.
def overlaps(start_a, end_a, start_b, end_b):
"""Return True if two time ranges overlap.
Ranges include the start hour and exclude the end hour, so a
booking that ends at 19 does not clash with one that starts at 19.
Args:
start_a: Start hour of the first range.
end_a: End hour of the first range (greater than start_a).
start_b: Start hour of the second range.
end_b: End hour of the second range (greater than start_b).
Returns:
True if the ranges share at least one hour, otherwise False.
>>> overlaps(18, 20, 19, 21)
True
>>> overlaps(18, 19, 19, 20)
False
"""
return start_a < end_b and start_b < end_a
class Booking:
"""One reservation of the ice surface.
Attributes:
team: Name of the team or group.
start: First hour booked.
end: Hour at which the booking ends (not included).
"""
def __init__(self, team: str, start: int, end: int) -> None:
"""Create a booking that fits within opening hours.
Raises:
ValueError: If the booking is outside opening hours or
end is not after start.
"""
if not (OPEN_HOUR <= start < end <= CLOSE_HOUR):
raise ValueError("booking must be within opening hours")
self.team = team
self.start = start
self.end = end
def clashes_with(self, other: "Booking") -> bool:
"""Return True if this booking overlaps the other booking."""
return overlaps(self.start, self.end, other.start, other.end)
if __name__ == "__main__":
# Self-test: run the examples in the docstrings, then a quick demo.
import doctest
print(doctest.testmod())
a = Booking("Bantam A", 18, 20)
b = Booking("Figure skating club", 20, 22)
print(a.clashes_with(b))
print(overlaps.__doc__.splitlines()[0])

arena.py, run directly.

TestResults(failed=0, attempted=2)
False
Return True if two time ranges overlap.

Output of the program above (what Python prints).

Documenting interfaces and hierarchies

When classes form a hierarchy, the documentation has to work across classes:

  • The abstract method's docstring is the contract. It states what every override must accept and guarantee: "Return the area in square units (a float >= 0)." Callers rely on it without knowing which subclass they have; that is what makes polymorphism safe.
  • The superclass docstring has a "Subclass contract" section: which methods to override, and any rules (no side effects, call super().__init__ first).
  • An override documents only what is different ("Overrides Shape.area"). If an override adds nothing new, it can skip the docstring: help() and inspect.getdoc fall back to the inherited one.
  • An override must never break the contract. If its docstring would have to say "unlike Shape.area, this may return a negative number", the design is wrong, not the documentation.
Shape.area() abstractdocstring = the CONTRACT:return a float >= 0, no side effectsCircle.area()own docstring:how THIS version worksSquare.area()no docstring:help() shows Shape'sEvery override must keep the contract written on the abstract method.
Documenting an interface. The abstract method's docstring states the contract every subclass must keep; an override documents only what is different, and documentation tools fall back to the inherited docstring when an override has none.
Worked example

Example 2: Documenting a class hierarchy

Document a small shape hierarchy so that a teammate could add a new shape without reading any method bodies. Mark the public API, write the subclass contract, and show what documentation tools report for an override with and without its own docstring.

  1. The module docstring names the public API, and __all__ lists the same names, so readers and tools agree.
  2. Shape's class docstring has a Subclass contract section: override area(), return a float of at least 0, and change nothing.
  3. The abstract method area() carries the contract in its own docstring, so help(Shape.area) shows it.
  4. Circle.area has a docstring that says how this version works and that it overrides Shape.area.
  5. Square.area has none. inspect.getdoc(Square.area) finds the inherited docstring from Shape, which is still accurate because Square keeps the contract. (Square returns float(...) so that it really does return a float, as promised.)

The module and its output are below. A teammate adding a Triangle reads only the Shape docstrings to know exactly what to write.

"""shapes.py: an abstract Shape and two concrete shapes.
Public API: Shape, Circle and Square. Names starting with _ are internal.
"""
import inspect
import math
from abc import ABC, abstractmethod
__all__ = ["Shape", "Circle", "Square"]
class Shape(ABC):
"""A flat shape with an area.
Subclass contract:
Every subclass must override area(). It must return a float >= 0
and must not change the shape (no side effects).
"""
@abstractmethod
def area(self):
"""Return the area in square units (a float >= 0)."""
class Circle(Shape):
"""A circle with radius r (r >= 0)."""
def __init__(self, r):
self.r = r
def area(self):
"""Return pi times r squared. Overrides Shape.area."""
return math.pi * self.r ** 2
class Square(Shape):
"""A square with side length side (side >= 0)."""
def __init__(self, side):
self.side = side
def area(self): # no docstring: tools show Shape.area's
return float(self.side ** 2)
print(inspect.getdoc(Circle.area))
print(inspect.getdoc(Square.area))
print(__all__)

shapes.py: a documented interface. The first two output lines are what documentation tools show for each override.

Return pi times r squared. Overrides Shape.area.
Return the area in square units (a float >= 0).
['Shape', 'Circle', 'Square']

Output of the program above (what Python prints).

LevelDocumentExample
Package (__init__.py)one line per moduleschedule -- keeps a day's bookings clash-free
Modulepurpose, conventions, public API__all__ = ["Booking", "Practice"]
Classwhat it represents, attributes, invariantInvariant: start < end.
Abstract methodthe contract for every overrideReturn the price in cents (>= 0).
Overrideonly what is differentAdds a 200-cent cleaning fee.
Public methodArgs, Returns, Raises, side effectsRaises: ValueError: if it clashes.
Internal helpera one-line docstring or comment# sort key: start hour
What to document at each level of a modular, object-oriented program.

Changing a documented interface

Once other modules depend on a public method, its documented behaviour is a promise. If you must replace it, do not delete it at once: keep it working, add a line such as "Deprecated since 1.1: use request() instead." to its docstring, and (optionally) call warnings.warn(..., DeprecationWarning) inside it. Record the change in the changelog, and remove the old method only in a later version. The Modifying Existing Modular Code lesson uses the same idea.

Reading the documentation of a whole program

  • help(arena) in the Python shell lists a module's docstring, its classes (with their method resolution order and inherited methods) and its functions.
  • help(Booking) shows the class docstring, every method docstring and the properties.
  • python -m pydoc -w arena writes the same information as a web page, arena.html; larger projects use tools such as Sphinx to build a documentation website from all their docstrings.
  • python -m doctest arena.py -v runs every >>> example in the module and reports each one.
Just for fun

Comment found in real code bases (and widely shared by programmers): "When I wrote this, only God and I understood what I was doing. Now, only God knows."

A docstring would have helped.

Hands-on activity

The documentation swap

You need: a multi-file program of your own with at least one class (or the rental shop, shapes or library examples from this unit), a computer with Python, and a family member or friend (no programming knowledge needed).

  1. Give the package or main folder a map: a docstring in __init__.py (or a README section) with one line per module.
  2. Give each module a docstring and an __all__ list. Rename internal helpers with a leading underscore.
  3. Give each class a docstring with its attributes and invariant. If you have an abstract class or a superclass, write its subclass contract.
  4. Run help() on one module and print or show the result. Ask your helper to tell you, from the help output alone, which class to use for a task you describe, and what would happen if they broke a rule (for example, a negative amount).
  5. Wherever they hesitate, improve the docstring. Finally, run python -m doctest on each module.

What to look for: a non-programmer can pick the right class from the help output; every invariant and every exception is written down; all doctests pass.

Your turn

1

What should a package docstring in __init__.py contain?

2

Write a class docstring for SavingsAccount (the account whose balance in cents can never go below zero).

3

What is the purpose of __all__ = ["Shape", "Circle", "Square"] in shapes.py?

4

Write the contract docstring for an abstract method cost(self) in a Booking superclass.

5

An override Party.cost has no docstring. What does help(Party.cost) or inspect.getdoc(Party.cost) show?

6

Why should an internal helper like _start_of(booking) get less documentation than Schedule.add?

7

A subclass's override would need the docstring "Unlike Booking.cost, this may return a negative price." What does that tell you?

More practice: warm-up

1

Where does a class docstring go?

2

What does a leading underscore on a name tell readers?

3

Which section lists a class's main data in Google style?

4

Which standard-library function returns a docstring and falls back to the inherited one?

5

What command writes a module's documentation as a web page?

More practice: on your own

1

Write a module docstring and __all__ for arena/schedule.py from the Putting It Together lesson (it offers the Schedule class).

2

Write the subclass contract section for an abstract Animal class whose subclasses must override sound().

3

Write a docstring for Helmet.cost(self, days), which overrides Item.cost and adds a 200-cent cleaning fee.

4

A method check_out(title, borrower) is being replaced by lend(item, borrower). Write the deprecation line for its docstring.

5

Why document a class's invariant if the code already enforces it?

Challenge

1

Write a fully documented abstract class PricingPolicy for the strategies in the Designing for Reuse lesson, including the subclass contract and a doctest that uses a tiny subclass.

2

Explain why the documentation of an abstract method is more important than the documentation of any one override.

3

Your team's module exposes 30 functions, but other modules only use 6. Propose a documentation and naming plan.

Check your understanding

Quiz

1.

What is the main job of a package docstring in __init__.py?

2.

Where should the rules that every subclass must follow be written?

3.

An override has no docstring. What does inspect.getdoc return for it?

4.

What does __all__ declare?

5.

Which belongs in a class docstring?

6.

A public method is being replaced. What is the best documentation practice?

For parents and teachers

Big idea: A4.3 asks for fully documented code to industry standards. The Data Types, Files and Expressions unit taught the basics (PEP 8, PEP 257, Google-style sections, comments, doctest). This lesson applies them to modular, object-oriented programs: package maps, module APIs, class docstrings with invariants, and contracts for class hierarchies, which are what make code reusable by others (C1.4).

How to help: You do not need to know Python for the documentation swap: read only the help() output and say which class you would use and what rules it has. If you cannot tell, the documentation needs work.

Common difficulties:

  • Documenting every method in detail but never the class or module as a whole.
  • Leaving the subclass contract unwritten, so new subclasses break assumptions.
  • Overrides whose docstrings contradict the superclass.
  • Forgetting to update __all__ and docstrings when public names change.