Reading Documentation Without Getting Lost
Docs are a reference, not a book, so the skill is finding the one page you need
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:
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.
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(","))
split() ['ada', '36', 'oxford']
split(" ") ['', '', 'ada', '', '', '36', '', '', 'oxford', '', '']
split(None,1) ['ada', '36 oxford ']
"a,b,,c" ['a', 'b', '', 'c']
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))
With an explicit separator, every occurrence splits and empty fields are kept — so "ada,,36,oxford" has four fields, one of them empty. maxsplit=1 stops after the first comma and hands back the rest of the string untouched, commas and all. With no separator at all, runs of whitespace collapse and the ends are trimmed.
# str.split(sep=None, maxsplit=-1) -> predict each result, then run it.
LINE = "ada,,36,oxford"
PREDICTIONS = {
'LINE.split(",")': ["ada", "", "36", "oxford"],
'LINE.split(",", 1)': ["ada", ",36,oxford"],
'LINE.split(",", 2)': ["ada", "", "36,oxford"],
'" a b ".split()': ["a", "b"],
}
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))
LINE.split(",") right ['ada', '', '36', 'oxford']
LINE.split(",", 1) right ['ada', ',36,oxford']
LINE.split(",", 2) right ['ada', '', '36,oxford']
" a b ".split() right ['a', 'b']
right: 4 of 4
The four questions to ask any page
Once you are on the right page, you are looking for four things. In this order:
- What does it give me back? A list, a generator, a new object, or
Nonebecause it changed something in place. Getting this wrong is the classicx = my_list.sort()bug. - What can go wrong? Which exception does it raise, and when. This is what your
exceptclause needs to name. - What are the defaults? The signature line answers this, and the defaults are where the surprises live.
- 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.
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)
[('a', 'b', 'c'), ('d', 'e', 'f'), ('g',)]
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:
- 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.
- 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.
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])
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.
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
It is laid out like prose, so it looks like it should be read like prose.
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
They are printed in small italics next to things you do want to read.
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
It works, and the task is finished.
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
Reference prose front-loads precision, so the opening sentence is often the hardest one on the page.
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
A blog post is written for a person, and the docs are written for correctness.
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?
str.split(sep=None, maxsplit=-1) tells you the parameter names, that both are optional, and exactly what happens if you pass neither.
Why check version notes on a documentation page?
itertools.batched is new in 3.12. On 3.11 the import fails outright, which is why the guarded import in this lesson exists.
What is the right way to use an example from the docs?
The prediction does the teaching. A wrong prediction has located a gap in your understanding, which is more valuable than a right one.
What does the default sep=None mean for str.split?
It selects a genuinely different algorithm from split(" "): whitespace runs collapse and leading or trailing whitespace produces no empty strings. This one detail prevents a very common bug.
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")
inspect.signature(func).parameters is an ordered mapping of name to Parameter. A parameter is required when parameter.default is inspect.Parameter.empty. Then walk arguments for names that are not in parameters at all. Sort once at the end.
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."""
parameters = inspect.signature(func).parameters
problems = []
for name, parameter in parameters.items():
if parameter.default is inspect.Parameter.empty and name not in arguments:
problems.append(f"missing: {name}")
for name in arguments:
if name not in parameters:
problems.append(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")
['recipient', 'subject'] -> ok
['recipient', 'retries', 'subject'] -> ok
['subject', 'urgent'] -> ['missing: recipient', 'unexpected: urgent']
assert call_problems(send_report, {"recipient": "a", "subject": "b"}) == [], "the two required parameters are enough; body and retries have defaults"
assert call_problems(send_report, {"subject": "b"}) == ["missing: recipient"], "a parameter with no default is required"
assert call_problems(send_report, {"recipient": "a", "subject": "b", "urgent": True}) == ["unexpected: urgent"], "a name the signature does not list would raise TypeError"
assert call_problems(send_report, {}) == ["missing: recipient", "missing: subject"], "report every missing one, sorted"
assert call_problems(send_report, {"recipient": "a", "subject": "b", "retries": 1}) == [], "retries is keyword-only but it does have a default"
def one(a, b=2):
pass
assert call_problems(one, {"a": 1}) == [], "it must read any function's signature, not just send_report"
print("\u2713 Looks good!")