Reading documentation and other people's code
Find reliable answers in official documentation, read function signatures, and make sense of unfamiliar code by searching and tracing it.
What you will learn
- Tell reference documentation from tutorials and know when to use each
- Read a function signature and the parts of a reference entry
- Match documentation to the version you are actually running
- Search an unfamiliar codebase and trace how a value flows through it
On this page
Nobody remembers every function of a language, and nobody writes programs from a blank page. Professional programming is mostly reading: reading the documentation of tools you use, reading the code your team wrote last year, reading the error that just appeared. This lesson teaches how to read well, because it is faster than guessing and more reliable than a half-remembered forum answer.
Kinds of documentation
Documentation comes in several kinds, and each answers a different question.
| Kind | Answers | Example |
|---|---|---|
| Tutorial | “How do I get started?” Teaches in order, with explanations | A language’s official tutorial |
| How-to guide | “How do I do this specific task?” | “How to read a file line by line” |
| Reference | “What exactly does this function do?” Complete, precise and dry | The page for str.split |
| Explanation | “Why is it designed this way?” | A design document, a language specification |
When you are new to a topic, start with a tutorial. When you know roughly what you need and want the exact rules, go to the reference. Reference pages are not meant to be read from start to finish. You look something up, read that entry carefully, and leave.
The most trustworthy source is the official documentation for the language or library: the one published by the people who maintain it. Blog posts and forum answers can be helpful, but they may be wrong, incomplete or out of date. Use them to find the name of the thing you need, then confirm the details in the official reference.
Where to find it for the four languages in this track:
| Language | Official reference | From the terminal |
|---|---|---|
| Python | The Python documentation: the Library Reference and the Language Reference | help(sorted) inside the Python REPL, or python -m pydoc sorted |
| JavaScript | The ECMAScript specification is the formal source; most people read MDN Web Docs | No standard command |
| Java | The Java API documentation (Javadoc) for your Java version | javap java.lang.Math lists the members of a class |
| C | The C standard, and the manual pages of your system | man 3 round |
Anatomy of a reference entry
A good reference entry has a regular shape. Here is the Python function sorted, asked for its signature:
import inspect
print("sorted", inspect.signature(sorted))print("str.split", inspect.signature(str.split))print("round", inspect.signature(round))
words = ["pear", "fig", "banana"]print(sorted(words, key=len, reverse=True))print("a,b,c,d".split(",", 2))print(round(3.14159, 2))Output:
sorted (iterable, /, *, key=None, reverse=False)str.split (self, /, sep=None, maxsplit=-1)round (number, ndigits=None)['banana', 'pear', 'fig']['a', 'b', 'c,d']3.14The exact text of a signature can change a little between Python versions, which is a good example of why you read the documentation for the version you run. A signature is a compact summary, and you can learn to read it:
- The name says what it does:
sorted. - Parameters in order:
sortedtakes aniterable(anything you can loop over), which it reads. - Defaults. A parameter with
=is optional.key=Nonemeans you may omit it;Nonemeans “no custom key”.reverse=Falsemeans ascending order unless you ask otherwise. A parameter without a default, likeiterable, is required. - Special markers. In Python,
/and*control how arguments may be passed: parameters before/must be given by position, and those after*must be given by name. That is whysorted(words, key=len)works butsorted(words, len)does not. - Return type. Signatures in typed languages state it directly; in Python the entry’s text says it (“Return a new sorted list from the items in iterable”).
The calls below show the three signatures at work. sorted(words, key=len, reverse=True) sorts by length, longest first. "a,b,c,d".split(",", 2) passes maxsplit=2, so it makes at most two splits and the last piece keeps its comma. round(3.14159, 2) keeps two digits.
After the signature, read the description for what happens in the unusual cases: empty input, missing values, out-of-range values. Then read the notes, exceptions raised and examples. Finally, look for a “See also” section, which often leads to the function you really wanted.
The same name, different rules
Never assume a function behaves the same way in another language because it has the same name. Here is a famous trap: rounding. The program rounds four numbers, each exactly halfway between two whole numbers, so the language’s tie-breaking rule decides the answer.
Python
for x in [0.5, 1.5, 2.5, -2.5]: print(f"{x} -> {round(x)}")Output:
0.5 -> 01.5 -> 22.5 -> 2-2.5 -> -2JavaScript
for (const x of [0.5, 1.5, 2.5, -2.5]) { console.log(`${x} -> ${Math.round(x)}`);}Output:
0.5 -> 11.5 -> 22.5 -> 3-2.5 -> -2Java
public class DocsRound { public static void main(String[] args) { double[] values = {0.5, 1.5, 2.5, -2.5}; for (double x : values) { System.out.println(x + " -> " + Math.round(x)); } }}Output:
0.5 -> 11.5 -> 22.5 -> 3-2.5 -> -2C
#include <math.h>#include <stdio.h>
int main(void) { double values[] = {0.5, 1.5, 2.5, -2.5}; for (int i = 0; i < 4; i++) { printf("%.1f -> %.0f\n", values[i], round(values[i])); } return 0;}On some systems you must link the math library: gcc docs_round.c -lm.
Output:
0.5 -> 11.5 -> 22.5 -> 3-2.5 -> -3Four languages, one name, three different answers. Python’s round rounds halves to the nearest even number (so 0.5 becomes 0 and 2.5 becomes 2). JavaScript’s Math.round and Java’s Math.round round halves towards positive infinity (so -2.5 becomes -2). C’s round rounds halves away from zero (so -2.5 becomes -3). Each language documents its rule on the page for the function. Only the documentation of the language you are writing in tells you what your code does, so when behaviour matters, read the entry.
Match the version
Documentation describes a specific version. A function can be added, changed or removed between releases, and an answer written for an older version may not work on yours. Check which version you run (python --version, node --version, java -version, gcc --version) and use the matching documentation. Most documentation sites have a version picker, and the pages usually note when a feature appeared (“New in version 3.9”) or was removed. The language tracks mark this with a Since tag wherever it matters.
Reading other people’s code
The skills for reading code are different from those for writing it. You are not trying to understand every line. You are trying to answer a question: how does this feature work? Where is this value set? Why does this return nothing?
- Start from the entry point. Find where the program starts (
mainin C and Java, the top-level script in Python and JavaScript). Read the outline before the detail. - Search, do not scroll. Search for the name of the function, the error message or the text you see on screen. From a terminal,
grep -rn "parse_age" .lists every line in every file below the current folder that containsparse_age, with file name and line number. Your editor’s “find in files” and “go to definition” do the same. Version control can also tell you who changed a line and why:git log -p filenameshows the commit messages and diffs of a file. - Read the tests. Tests show how the code is meant to be used, with example inputs and outputs, and they are often clearer than the code.
- Read the signature and the name before the body. Good names tell you what a function takes and returns.
- Trace a value. Pick one input and follow it through the code, as in the hand-tracing of the first lesson, writing the variables down as they change.
- Run it. Add a print, or run the function in a REPL with a small input and look at the result. Observing is quicker than reasoning when the code is unfamiliar.
- Take notes as you go. Write down what each function does in your own words. When you can explain it without looking at it, you understand it.
Here is a short function with no comments. The signature suggests a list and a size, but read the body and trace a call to find out what it does:
def mystery(items, size): result = [] for start in range(0, len(items), size): result.append(items[start:start + size]) return result
print(mystery([1, 2, 3, 4, 5, 6, 7], 3))print(mystery([], 3))print(mystery(["a", "b"], 5))Output:
[[1, 2, 3], [4, 5, 6], [7]][][['a', 'b']]Tracing the first call: start takes the values 0, 3 and 6, and each time the slice of up to 3 items is appended. The function cuts a list into chunks of at most size items; the last chunk may be shorter, and an empty list gives an empty result. A better name would be chunks. When you understand code like this, rename it or write a short comment, so the next reader does not have to repeat the work.
Asking for help well
Sometimes you do need to ask a person or a forum. A good question gets a good answer quickly. State what you are trying to do, show the smallest code that reproduces the problem (the isolation step from the debugging lesson), give the exact error message, the language and version, and what you have already tried. Never paste only a screenshot of a stack trace when you can paste the text.
Common pitfalls
- Trusting the first search result. Check the date, the version and whether it is official.
- Copying code you do not understand. If you cannot explain it, you cannot debug it.
- Assuming a name means the same thing everywhere.
Math.round,splitandsortdiffer between languages in the details that cause bugs. - Reading the wrong version. An answer for an old version can fail on the new one, and the other way round.
- Skipping the description. The signature gives the shape, but the text gives the edge cases, such as what happens with empty input.
- Reading everything. Stay focused on your question. You do not need to understand a whole codebase to fix one bug in it.
Use only the official Python documentation (for example the page for each method named) to predict what each of these expressions gives. Then check the prediction by running the code.
"hello".find("z")"a b c".split()and"a b c".split(" ")[3, 1, 2].sort()round(2.675, 2)max([], default=0)
Show solution
print("1:", "hello".find("z"))print("2:", "a b c".split(), "a b c".split(" "))print("3:", [3, 1, 2].sort())print("4:", round(2.675, 2))print("5:", max([], default=0))Output:
1: -12: ['a', 'b', 'c'] ['a', 'b', '', 'c']3: None4: 2.675: 0- The
str.findentry says it returns -1 if the substring is not found. - The
str.splitentry says that with no separator, runs of whitespace count as a single separator. With" "as the separator, each space splits, which produces an empty string between the two spaces. list.sortsorts the list in place and returnsNone. It is a common mistake to writex = items.sort(). The functionsortedreturns a new list instead.- 2.675 cannot be stored exactly as a binary float; the stored value is slightly below 2.675, so it rounds down to 2.67. The
roundentry has a note about this; it is the imprecision from the operators lesson. - The
maxentry saysdefaultis the value returned when the iterable is empty. Without it,max([])raises aValueError.
Use this signature for a made-up function: def send(message, to, *, retries=3, urgent=False). Which of these calls are valid? Which are not, and why?
send("hi", "Ada")send("hi", "Ada", urgent=True)send("hi", "Ada", 5)send("hi")send(to="Ada", message="hi", retries=0)
Show solution
- Valid.
messageandtoare given, and the other two use their defaults. - Valid.
urgentis a keyword argument. - Invalid. The
*means the parameters after it can only be passed by name, so a third positional argument is rejected (retries=5would work). - Invalid.
tohas no default, so it is required. - Valid. Parameters before the
*may be passed by name in any order, andretriesis passed by name.
Read this function and write one sentence describing what it does. Then say what it returns for [3, 1, 4, 1, 5], and for [].
def mystery2(numbers): best = None for n in numbers: if best is None or n > best: best = n return bestShow solution
It returns the largest number in the list, or None if the list is empty. For [3, 1, 4, 1, 5] it returns 5: best starts as None, becomes 3, stays 3 while 1 is skipped, becomes 4, skips 1, then becomes 5. For [] the loop never runs and it returns None. A better name is largest. The built-in max does the same job but raises an error on an empty list unless you pass default.
Recap
- Match the kind of documentation to your question: tutorials to learn, reference to look up exact behaviour. Prefer the official source and the version you run.
- A signature tells you the parameters, their order, which are optional, and how they may be passed. The description tells you the edge cases.
- The same function name can behave differently across languages (the rounding of halves is different in Python, JavaScript, Java and C), so confirm in the docs.
- To read unfamiliar code: start at the entry point, search instead of scrolling, read the tests, trace one value, and run it.
The final lesson draws the module together: clean code and security basics.