PYTH-290 · Lesson 11 of 12 · 25 min

Documentation and handover

▶ Video coming soon A video version of this lesson is on the way.

At the end of the term, Sam's co-op placement ends, Nia returns to college, and Jordan goes back to running Ridgeline's other systems. Ridgeline Ops has to keep working after the people who built it have moved on. Someone will need to fix a bug in March, add the four-weekly frequency in April, and restore from a backup on a bad day in the summer. Whether they can do that depends almost entirely on what the team wrote down, and on how well they handed over what they knew.

Documentation is often treated as a chore left for the last day. The capstone team spread it through the project, writing decision records as they decided, docstrings as they coded and the runbook during the security review, so the final week was for organizing and checking rather than writing everything from scratch. This lesson covers who documentation is for, what each audience needs, how to keep it accurate, and how to hand a system over.

Know your readers

Good documentation starts with the reader, not the writer. Ridgeline Ops has four groups of readers with different needs. Staff users, Theo, Kai, Mei and Anita, need to get tasks done: how to pack the morning orders, how to sign up a regular, what to do when an order is short of stock. Operators need to keep the system running: how to deploy, restore, rotate keys and respond to alerts. Developers need to change the code safely: how to set up the project, where things live, how to run the tests, why the design is the way it is. And API clients, starting with the web shop's developer, need to know exactly what each endpoint accepts and returns.

A useful model for organizing documentation is the one often called Diátaxis, which separates four kinds of document because they answer different questions. Tutorials teach a beginner by walking them through a complete small task. How-to guides help someone who already knows the basics achieve a specific goal. Reference material describes the system precisely, such as every endpoint or setting. Explanation discusses background and reasons, such as why the project uses SQLite. Mixing the kinds produces documents that serve nobody well: a reference page cluttered with stories, or a tutorial that stops to list every option.

A grid of the Ridgeline Ops documentation. Rows are the readers: staff, operators, developers and API clients. Columns are the kinds of document: tutorial, how-to guide, reference and explanation. Cells name the documents, such as a staff quick-start, how to pack the morning orders, the runbook, the API reference, the getting started guide for developers and the decision records.
The documentation set, organized by reader and by kind of document

The README

The README is the front door of the repository, and the first thing a new developer reads. The team's README answered, in order: what is this, who is it for, how do I run it on my machine, how do I run the tests, how is it deployed, where is the rest of the documentation, and who to ask. It linked to longer documents rather than containing them. A README that takes more than a few minutes to read stops being read.

The team tested the README the only way that works: they asked someone who had never seen the project to follow it. Lena's colleague, a developer at another company, cloned the repository on a Thursday afternoon with the README open and a stopwatch running. It took her fifty minutes to get the tests passing, against a target of thirty, and she wrote down every place she got stuck: an undocumented environment variable, a command that assumed a Mac, a step that needed a tool nobody had mentioned. Each fix took a few minutes. Without the test, every future developer would have lost that time.

Try it yourself

Check the README

Write missing_sections(readme, required) that returns the required section names that do not appear as level-two headings (lines starting with "## ") in the Markdown text readme, in the order given in required.

Compare headings without regard to case or surrounding spaces, and ignore anything after a colon in a heading, so "## Running the tests: quick version" counts as "Running the tests". Headings inside fenced code blocks (between lines that start with three backticks) do not count.

def missing_sections(readme, required):
    return [name for name in required if name not in readme]

README = """# Ridgeline Ops
## What this is
Subscriptions for Ridgeline.
## Getting started
Clone the repository.
"""
print(missing_sections(README, ["What this is", "Getting started", "Running the tests"]))

Guides for staff

Staff did not want a manual. They wanted short how-to guides for the handful of tasks they do, written in their words and kept where they work. Nia wrote one page per task, each starting with the goal and ending with what success looks like: Pack this morning's subscription orders; Sign up a regular at the till; Handle a short-stock warning; Check yesterday's subscription revenue. Each page had numbered steps, a screenshot of the screen at the key moment, and a short section called If something goes wrong. The pages lived in the staff section of Ridgeline Ops itself, linked from the screen they describe, so Theo could open the packing guide from the packing list page.

The guides used the language of the business, not of the code. The code says renewal orders and status codes; Theo says subscription bags and something went wrong. Nia read each guide aloud to the person who would use it and changed every word they stumbled on. The packing guide went through three versions before Theo said it matched what he actually does.

Reference from the code

Reference documentation goes out of date faster than anything else, because it describes details that change with every release. The team's answer was to generate it from the code wherever possible, so that changing the code changes the documentation. The API reference was produced from the route definitions and their docstrings, plus the JSON schemas the validation already used. Configuration settings were documented from the configuration loader. The command-line help came straight from argparse.

Docstrings were the source for most of this, so the team agreed a style: a one-line summary in the imperative mood, a blank line, then details where needed, including the errors a function raises. Docstrings describe what a function promises and why it exists, not how it works line by line, which the code already says.

Generating an API reference table from routes and docstrings
import inspect

def skip(sub_id, today):
    """Skip the next delivery of a subscription.

    Raises SkipRefused if the delivery is already packed, and
    PauseSuggested after two skips in a row.
    """

def pause(sub_id):
    """Pause deliveries until the subscriber resumes."""

ROUTES = [("POST", "/api/v1/subscriptions/{id}/skip", skip),
          ("POST", "/api/v1/subscriptions/{id}/pause", pause)]

print("| Endpoint | Summary |")
print("|---|---|")
for method, path, handler in ROUTES:
    doc = inspect.getdoc(handler) or ""
    summary = doc.splitlines()[0] if doc else "(undocumented)"
    print(f"| {method} {path} | {summary} |")
Output
| Endpoint | Summary |
|---|---|
| POST /api/v1/subscriptions/{id}/skip | Skip the next delivery of a subscription. |
| POST /api/v1/subscriptions/{id}/pause | Pause deliveries until the subscriber resumes. |

Try it yourself

Generate the reference

Write reference(routes) where routes is a list of (method, path, handler) tuples. Return a Markdown table as a string: a header line "| Endpoint | Summary | Errors |", a separator line "|---|---|---|", then one line per route sorted by path then method.

The summary is the first line of the handler's docstring (from inspect.getdoc), or "(undocumented)" if there is none. Errors lists, in the order they appear, every word in the docstring that ends with "Refused", "Suggested" or "NotFound", joined with ", ", or "none". Lines end with "\n".

def reference(routes):
    lines = ["| Endpoint | Summary | Errors |"]
    for method, path, handler in routes:
        lines.append(f"| {method} {path} | {handler.__doc__} | none |")
    return "\n".join(lines)

Documentation that tests itself

Examples in documentation are the parts readers trust most and copy most often, so wrong examples do real harm. Python's doctest module runs the examples written in docstrings as interactive sessions and checks that the output matches. The team used doctests for the small pure functions in the domain, such as the date and money helpers, where an example is the clearest explanation and also a useful test. They ran in the pipeline alongside the other tests, so an example that stopped being true failed the build.

Try it yourself

Write an example that runs

Write bags_to_green_kg(bags) that returns the kilograms of green coffee Kai needs to roast for a number of 340 g bags, given that roasting loses about 15 in every 100 of the weight: green kg = bags x 0.34 / 0.85, rounded to 2 decimal places.

Give it a docstring with a one-line summary and at least two doctest examples, one of which must be >>> bags_to_green_kg(30) with its correct output. The tests run your docstring with doctest and check the function itself.

def bags_to_green_kg(bags):
    return bags * 0.34

Keeping documentation alive

Documentation decays the moment the code changes and the words do not. The team used three habits to slow the decay. First, documentation was part of the definition of done from lesson 4: a story that added a screen, a setting or an endpoint was not finished until the matching guide, setting description or reference entry was updated, and reviewers checked for it the same way they checked for tests. Second, documentation lived in the repository next to the code, written in Markdown and reviewed in the same pull requests, rather than in a separate wiki that nobody remembers to update. A change and its documentation arrive together or not at all.

Third, the team kept a changelog in the Keep a Changelog style from PYTH-260: a file listing each release with its date and the changes grouped as added, changed, fixed and removed, written for people rather than copied from commit messages. Theo and Anita read the staff-facing lines of the changelog before each release so nothing surprised them on the shop floor. When Rosa asked in January when the pickup option had been added and why, the changelog and the decision records answered in under a minute.

Some documentation is better deleted than kept. Early design notes, superseded plans and half-finished guides confuse readers who cannot tell what is current. The team moved such documents into an archive folder with a note at the top saying they are historical, and deleted anything that was simply wrong. A short, accurate set of documents beats a long one that contradicts itself.

Explaining the why

The decision records from lesson 3 were the team's main explanation documents, and by the end of the term there were sixteen of them. A future developer who wonders why subscriptions are served oldest first when stock is short, why the system runs one instance, or why skips the day before delivery are refused will find the reason, the alternatives and the date. The team also wrote a two-page architecture overview: the context diagram, the layers, a walk through one request from the web shop to the database and back, and a list of the places most likely to need change, with advice for each.

That last list is unusually valuable and rarely written. The people who built a system know where its weak spots are: the renewal job's stock allocation will need rethinking if mixed boxes are added; the single SQLite instance will need replacing if load grows; the tax rates are configuration, but adding a province or state with different rules for coffee beans will need code. Writing these down turns the builders' instincts into a map for the next person.

Handing over

Handover is more than leaving documents behind. In the final week the team ran three short sessions with the people who would own Ridgeline Ops next: Jordan for development, Sam Ortiz for operations, and Rosa for priorities. In each session, the new owner did the work while a builder watched, rather than the other way round: Sam Ortiz deployed a small change, restored the staging database from backup and rotated the shop key with the runbook in hand; Jordan, who had led the project but not written every part, fixed a small bug in the renewal job that Sam Okoye had deliberately left in the backlog for the purpose. Every hesitation became an improvement to the documents.

The team also checked the bus factor, a blunt name for the number of people who would have to disappear before nobody understood a part of the system. For each module they listed who could change it confidently. The roast plan export had a bus factor of one, Nia, so she paired with Jordan on its last change. Finally, they wrote down what was unfinished: the open backlog with Rosa's priorities, known bugs with their severity, and the ideas on the Later list from lesson 1. An honest list of what is not done is part of a good handover, not an admission of failure.

Common mistakes

  • MistakeWriting documentation only in the last days of the project.

    InsteadWrite decision records, docstrings and runbooks as the work happens, and organize them at the end.

  • MistakeWriting one long manual for everyone.

    InsteadWrite for each reader, and separate tutorials, how-to guides, reference and explanation.

  • MistakeAssuming the README works because it works for the authors.

    InsteadHave someone new follow it with a stopwatch and fix every place they get stuck.

  • MistakeMaintaining reference documentation by hand.

    InsteadGenerate it from the code, docstrings and schemas so it changes when the code changes.

  • MistakeLeaving examples that nobody runs.

    InsteadMake examples doctests and run them in the pipeline.

  • MistakeHanding over by giving a presentation.

    InsteadHave the new owners do real tasks while the builders watch, and record what is unfinished.

Summary and key terms

  • A system must keep working after its builders leave; documentation and handover make that possible.
  • Write for specific readers: staff, operators, developers and API clients need different things.
  • Tutorials, how-to guides, reference and explanation answer different questions and are kept separate.
  • The README is the front door; test it by having someone new follow it.
  • Staff guides are short, task-based and written in the business's language.
  • Generate reference material from code and docstrings, and run documentation examples as doctests.
  • Decision records and an architecture overview explain the why, including where change is likely.
  • Handover means new owners doing real tasks, checking the bus factor and listing what is unfinished.
README
The introductory document at the top of a repository, explaining what it is and how to start.
tutorial
A learning-oriented document that walks a beginner through a complete task.
how-to guide
A goal-oriented document of steps for someone who knows the basics.
reference documentation
Precise, complete descriptions of a system's parts, such as endpoints or settings.
docstring
A string at the start of a Python function, class or module that documents it.
doctest
An example in a docstring that Python can run and check automatically.
handover
Transferring responsibility for a system, and the knowledge needed, to new owners.
bus factor
The number of people who would have to leave before nobody understood part of a system.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Theo wants to know how to pack the morning orders. Which kind of document does he need?

    Show answer

    A how-to guide focused on that one task.

  2. Why did the team generate the API reference from the code?

    Show answer

    So it changes whenever the code changes and cannot drift out of date.

  3. A new developer took 50 minutes to get the tests running against a 30-minute target. What should the team do?

    Show answer

    Fix each place she got stuck in the README and setup, then test again.

  4. Write the first line of a docstring for pause(sub_id).

    Show answer

    For example: "Pause deliveries until the subscriber resumes."

  5. What does a bus factor of one for a module mean, and what is one remedy?

    Show answer

    Only one person understands it; pair another person with them on changes to spread the knowledge.

  6. Why is a list of unfinished work part of a good handover?

    Show answer

    It tells the new owners exactly what is left, so they can plan instead of discovering gaps by accident.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz