Reading Documentation Without Getting Lost

Docs are a reference, not a book, so the skill is finding the one page you need

Advanced 14 min

In this lesson

The single habit that separates people who keep improving from people who plateau is not talent, and it is not memory. It is being comfortable in documentation.

That is learnable, and it is mostly a set of small tricks: how to read a signature, which four questions to ask, and what to do with the example you find. This lesson is those tricks.

Explain it like I’m 5

Documentation is a manual, not a novel. Nobody reads it cover to cover — the skill is knowing how to find the one page that answers your question, and how to read that page quickly.

Nobody reads the manual front to back

If you have ever opened the Python docs, felt the wall of text, and closed the tab, the problem was the goal rather than the docs. “Learn pathlib” has no end point. “Find out whether Path.rename overwrites an existing file” has one, and takes ninety seconds.

So arrive with a question. Everything below assumes you have one.

A signature tells you most of what you need

Every reference entry starts with a signature line. It looks like decoration and it is actually the densest information on the page. Here is the one for a method you have used since Unit 1:

Example
str.split(sep=None, maxsplit=-1)

    Return a list of the words in the string, using sep as the delimiter
    string. If maxsplit is given, at most maxsplit splits are done (thus,
    the list will have at most maxsplit+1 elements). If maxsplit is not
    specified or -1, then there is no limit on the number of splits.

    If sep is not specified or is None, a different splitting algorithm is
    applied: runs of consecutive whitespace are regarded as a single
    separator, and the result will contain no empty strings at the start
    or end if the string has leading or trailing whitespace.
The opening of the str.split entry in the library reference.
Example
line = "  ada   36   oxford  "

print("split()      ", line.split())
print('split(" ")   ', line.split(" "))
print("split(None,1)", line.split(None, 1))
print('"a,b,,c"     ', "a,b,,c".split(","))
Output
split()       ['ada', '36', 'oxford']
split(" ")    ['', '', 'ada', '', '', '36', '', '', 'oxford', '', '']
split(None,1) ['ada', '36   oxford  ']
"a,b,,c"      ['a', 'b', '', 'c']
The same string, four calls, and the documentation was right.

Predict before you run. Fill in PREDICTIONS with what you think each call returns, working only from the signature str.split(sep=None, maxsplit=-1) and the paragraphs above. Then press Run and see how you did — a wrong prediction is worth more than a right one here, because it marks the exact thing you had misread.

# str.split(sep=None, maxsplit=-1)  ->  predict each result, then run it.
LINE = "ada,,36,oxford"

# TODO: replace each [] with the list you think comes back
PREDICTIONS = {
    'LINE.split(",")': [],
    'LINE.split(",", 1)': [],
    'LINE.split(",", 2)': [],
    '"  a  b  ".split()': [],
}

ACTUAL = {
    'LINE.split(",")': LINE.split(","),
    'LINE.split(",", 1)': LINE.split(",", 1),
    'LINE.split(",", 2)': LINE.split(",", 2),
    '"  a  b  ".split()': "  a  b  ".split(),
}

for call, predicted in PREDICTIONS.items():
    got = ACTUAL[call]
    print(f"{call:20s} {'right' if predicted == got else 'wrong':6s} {got}")

print("right:", sum(1 for call in PREDICTIONS if PREDICTIONS[call] == ACTUAL[call]),
      "of", len(PREDICTIONS))

The four questions to ask any page

Once you are on the right page, you are looking for four things. In this order:

  1. What does it give me back? A list, a generator, a new object, or None because it changed something in place. Getting this wrong is the classic x = my_list.sort() bug.
  2. What can go wrong? Which exception does it raise, and when. This is what your except clause needs to name.
  3. What are the defaults? The signature line answers this, and the defaults are where the surprises live.
  4. Which version is this? See below.

You can usually answer all four in under a minute, and answering them beats reading the page.

Version notes are not footnotes

Reference pages carry small notes: New in version 3.12, Changed in version 3.10, Deprecated since version 3.13. They are the difference between code that works and code that crashes on someone else's machine.

If you need something newer than your target version, the fix is a guard rather than a hope.

Example
import sys

# "New in version 3.12" is a real constraint, not a footnote.
if sys.version_info >= (3, 12):
    from itertools import batched
    rows = list(batched("abcdefg", 3))
else:
    letters = list("abcdefg")
    rows = [tuple(letters[i:i + 3]) for i in range(0, len(letters), 3)]

print(rows)
Output
[('a', 'b', 'c'), ('d', 'e', 'f'), ('g',)]
itertools.batched arrived in 3.12; this runs correctly on either side of that line.

Try the example in five lines, then change one thing

Documentation examples are written to be minimal, which makes them ideal to run. The technique is two steps and it is the fastest way to actually learn a function:

  1. Run it unchanged. If it does not work, the problem is your setup or your version, and finding that out now saves an hour of confusion.
  2. Change exactly one thing. One argument, one input value. Then predict what will happen before pressing Run.

The prediction is the part that does the teaching. Being wrong is the useful outcome: it means you have found a gap in your model of the function, which is precisely what you came for. Copying an example unchanged into your project teaches nothing and leaves you unable to fix it when the input differs.

Ask the object itself

You do not always need a browser. Python can describe its own objects, which is often quicker than a search and is always about the version you are actually running.

help(thing) prints the docstring, dir(thing) lists what it can do, and the inspect module gives you the signature in a form you can pick apart.

Example
import inspect

def clamp(value, low=0.0, high=1.0):
    """Keep a number inside a range.

    Anything below `low` comes back as `low`, anything above `high` as `high`.
    """
    return max(low, min(high, value))

signature = inspect.signature(clamp)
print("signature:", f"clamp{signature}")

for name, parameter in signature.parameters.items():
    if parameter.default is inspect.Parameter.empty:
        print(f"  {name:6s} required")
    else:
        print(f"  {name:6s} defaults to {parameter.default!r}")

print("summary:  ", inspect.getdoc(clamp).splitlines()[0])
Output
signature: clamp(value, low=0.0, high=1.0)
  value  required
  low    defaults to 0.0
  high   defaults to 1.0
summary:   Keep a number inside a range.
The same information a reference page would give you, from the code itself.

When a tutorial is the better tool

Reference documentation answers precise questions and is poor at teaching a subject from nothing. That is not a flaw; the tutorial and the HOWTOs are for that.

A workable division:

  • A new subject you know nothing about — a tutorial, a HOWTO, or a course. Something that decides the order for you.
  • A specific question about something you half-know — the reference. This is where most of your reading will end up.
  • “Why does my code do this?” — the reference, plus the debugging playbook.
  • “Is there a better way to do this?” — other people's code, and the standard library's own source.

For third-party libraries the same shape applies: a good library has a tutorial-style “getting started” page and a separate API reference, and knowing which one you need is half of using it well. A library that has only one of the two is telling you something, which is the subject of the next lesson.

Common mistake: Trying to read a reference page front to back

Why it happens:

It is laid out like prose, so it looks like it should be read like prose.

How to fix it:

Arrive with a question and use the page to answer it. Reference documentation is written to be complete, not to be read in order.

Common mistake: Ignoring version notes

Why it happens:

They are printed in small italics next to things you do want to read.

How to fix it:

Check “New in version” against the Python you are targeting, and guard the import with sys.version_info if you need something newer. This is the difference between working on your machine and working on a colleague's.

Common mistake: Copying an example without changing anything

Why it happens:

It works, and the task is finished.

How to fix it:

Change one argument and predict the result first. You will remember it, and you will be able to fix it when your real input turns out not to look like the example's.

Common mistake: Giving up because the first paragraph is dense

Why it happens:

Reference prose front-loads precision, so the opening sentence is often the hardest one on the page.

How to fix it:

Skip to the examples, get one running, and come back to the prose with something concrete in mind. The paragraph is usually easy on the second pass.

Common mistake: Trusting a search result over the official docs

Why it happens:

A blog post is written for a person, and the docs are written for correctness.

How to fix it:

Use the blog post to understand the shape, then confirm the details in the docs. Blog posts and highly-rated forum answers go stale silently; the reference for your version does not.

What is a function signature?

Why check version notes on a documentation page?

What is the right way to use an example from the docs?

What does the default sep=None mean for str.split?

Mini exercise (hard)

Read a signature the way a computer would. call_problems(func, arguments) takes any function and a dictionary of keyword arguments, and returns a sorted list of everything wrong with calling func(**arguments): "missing: name" for each required parameter that is absent, and "unexpected: name" for each argument the function does not accept.

Have a go. Finish the code and press Run to see the result immediately, right on this page.

import inspect

def send_report(recipient, subject, body="", *, retries=3):
    """Send one report.

    `recipient` and `subject` are required; `retries` is keyword-only.
    """
    return f"sent to {recipient}"

def call_problems(func, arguments):
    """Everything wrong with calling func(**arguments), read from its signature."""
    problems = []
    # TODO: required parameters that are absent -> f"missing: {name}"
    # TODO: names the function does not accept -> f"unexpected: {name}"
    return sorted(problems)

CALLS = [
    {"recipient": "[email protected]", "subject": "July"},
    {"recipient": "[email protected]", "subject": "July", "retries": 1},
    {"subject": "July", "urgent": True},
]

for arguments in CALLS:
    print(sorted(arguments), "->", call_problems(send_report, arguments) or "ok")

What to learn next

You can read a signature line for four facts before running anything, ask any page the four questions that matter, guard an import against a version that does not have it, run an example and change exactly one thing, and ask an object about itself with inspect. You also proved the sep=None paragraph right, since split() and split(" ") really are different algorithms, and wrote the signature check Python itself runs before every call.

Documentation is how you judge one library. Deciding whether to take it on at all is a different check: Is This Library Worth Installing? is the five minutes that decides whether your project still installs in three years.