Other Ways to open this interlude: JupyterLite | Colab | Read Only | Download
Ignore this cell — used when running JupyterLite.
from os.path import basename, exists
def download(url):
"""Download a file if it isn't already here, and return its filename."""
filename = basename(url)
if not exists(filename):
from urllib.request import urlretrieve
local, _ = urlretrieve(url, filename)
print("Downloaded " + str(local))
return filename
download('https://github.com/porttack/working-in-python/raw/v3/working_in_python.py');
import working_in_python
# apcsp:begin type="note" chapter="6b"
working_in_python.enable_docstring_reminders()
# apcsp:end
Docstrings and Doctests#
This interlude is not part of Think Python. It sits between Chapter 6 and Chapter 7 because of what you just learned: functions that return values. Until now, your functions drew pictures or printed text, and the only way to check them was to look. Now they hand something back, and something that gets handed back can be checked automatically.
In Chapter 4 you wrote your first docstring: a short piece of text at the top of a function that says what the function does. A docstring is a promise. It tells whoever reads your code, including you three weeks from now, what they should expect if they call it.
A promise that nobody checks is just a hope.
In this interlude we’ll add a second layer to that docstring: a few example calls, and the answer each one should give. Python can read those examples and run them. If the function keeps its promise, nothing happens. If it doesn’t, Python tells you exactly which promise was broken.
That second layer is called a doctest.
What a good docstring says#
Here is a function with no docstring at all.
def price_with_tax(price, rate):
return price * (1 + rate)
It probably works. But answer these without scrolling: is rate a percentage like
8.5, or a decimal like 0.085? Does the result get rounded? What happens if price
is negative?
You can’t answer any of those from the code alone, and neither can anyone else. A docstring answers them.
def price_with_tax(price, rate):
"""Compute a price including sales tax.
rate: tax rate as a decimal, so 8.5% is 0.085
Returns the total as a float, not rounded.
"""
return price * (1 + rate)
Three rules, and they’re the whole game:
A summary line that starts with a verb and says what the function does.
Document a parameter only when its name doesn’t already say it.
rateneeded explaining.pricedid not. A docstring that says “price: the price” wastes everyone’s time.Say what comes back, if anything comes back.
Purpose, function, input, output#
There is a four-part frame worth learning now, because you will be asked to write it out in April and because a good docstring already contains three of the four parts.
Part |
The question it answers |
Where it lives in the docstring |
|---|---|---|
Purpose |
Why would anyone want this? |
Nowhere. See below. |
Function |
What does it do when it runs? |
The summary line |
Input |
What data does it receive? |
The parameter notes |
Output |
What does it produce? |
The “Returns…” line |
Look back at price_with_tax. “Compute a price including sales tax” is the
output.
The part a docstring never carries is purpose. “Compute a price including sales tax” tells you what the code does. It does not tell you that this is for a school store checkout, that the volunteers kept getting the tax wrong by hand, and that the whole program exists so the line moves faster. That’s purpose, and it lives outside the function entirely.
Keep the distinction. Function is behavior, described in the third person. Purpose is the need being served. Students routinely answer the purpose question with a feature list, and a feature list is neither one.
A word about words. This book says function. The AP exam says procedure for the same thing, and the exam’s written response asks about a “student-developed procedure” by name.
Historically those two words meant genuinely different things. In Pascal, a
proceduredid some work and handed nothing back, while afunctioncomputed a value and returned it. Fortran drew the same line between aSUBROUTINEand aFUNCTION. The distinction was useful: the name told you whether a call was worth putting on the right-hand side of an assignment.Modern languages mostly abandoned it. Python has only
def, and a function that returns nothing quietly returnsNone. The exam uses procedure for both cases, so the older distinction is gone from its vocabulary too.Answer to both words. But it’s worth knowing the difference existed, because you will meet languages that still keep it, and because “does this hand something back?” remains the first question worth asking about any function you’re about to call.
The docstring is data, not a comment#
A # comment is invisible to the running program. A docstring isn’t. Python stores it
on the function itself, which means the program can read its own documentation.
help(price_with_tax)
price_with_tax.__doc__
This is why docstrings use triple quotes and go inside the function, on the first line. Move them anywhere else and they turn back into ordinary strings that nobody can find.
Your first doctest#
A doctest is an example call written inside the docstring, followed by the answer you
expect. Each example starts with >>>, which is the prompt Python shows when you type
at it directly.
def double(x):
"""Return twice the value of x.
>>> double(5)
10
>>> double(-3)
-6
"""
return 2 * x
Read those two lines as a sentence: if someone calls double(5), the answer should
be 10. That’s a claim about the function, written in a form Python can check.
To check it, we need a helper. Import it from working_in_python:
from doctest import run_docstring_examples
def run_doctests(func):
run_docstring_examples(func, globals(), name=func.__name__)
run_doctests(double)
Nothing happened. That’s the point: no news is good news. run_doctests only
speaks up when something is wrong.
Note for later.
run_doctestsis a convenience that exists because we’re working in notebooks. When you start writing.pyfiles, the same tests run a different way. We’ll get there.
When a test fails#
Here’s the same function with a bug. The docstring still makes the same promise.
def double_broken(x):
"""Return twice the value of x.
>>> double_broken(5)
10
>>> double_broken(-3)
-6
"""
return x + 2
run_doctests(double_broken)
Notice what the failure report gives you, in order:
which call failed
Expected, what the docstring promised
Got, what the code actually did
And notice which test caught it. double_broken(5) returns 7, so that one fails too –
but if the only test had been double_broken(2), the answer would have been 4, the test
would have passed, and the bug would have shipped.
Choosing test cases is not busywork. It’s the skill.
Choosing test cases#
Each of those >>> lines is a test case: one input, paired with the output it should
produce. That pairing is the whole definition, and it’s worth holding onto, because the
exam asks about test cases in exactly those terms.
A test case that always passes teaches you nothing. Aim for three kinds:
Kind |
What it checks |
Example for |
|---|---|---|
Typical |
The ordinary case anyone would try |
|
Boundary |
The value where behavior changes |
|
Edge |
The unusual input you suspect is wrong |
|
Typical cases are the ones students write. Boundary and edge cases are the ones they skip, and those are where the bugs live: zero, empty, one, the first, the last, the day the month rolls over.
def can_ride(height_inches, age):
"""Check whether someone may ride, given height and age.
Riders must be at least 48 inches tall and at least 7 years old.
>>> can_ride(52, 9)
True
>>> can_ride(48, 7)
True
>>> can_ride(72, 6)
False
"""
return height_inches >= 48 and age >= 7
run_doctests(can_ride)
The second test is the boundary: exactly 48 inches, exactly 7 years. If someone
later “fixes” the code by changing >= to >, that test fails immediately.
The third test is the interesting one. A very tall six-year-old is exactly the case where
a careless writer would have used or instead of and. A test case can be funny and
still be the most useful test in the file.
Tests as documentation#
Read these two descriptions of the same function:
Returns the portion of the string before the first separator, or the whole string if the separator does not occur.
versus
>>> first_field('name,age,city', ',')
'name'
>>> first_field('solo', ',')
'solo'
The prose is more complete. The examples are faster. Good documentation has both, and the examples are the part people actually read.
What doctests can’t check#
Doctests compare what your function produces against text you wrote by hand. That works beautifully when the answer is predictable, and not at all when it isn’t.
def average(a, b):
"""Return the mean of two numbers.
>>> average(1, 2)
1.5
>>> average(0.1, 0.2)
0.15000000000000002
"""
return (a + b) / 2
That second expected value looks like a typo. It isn’t. It’s the actual answer, and you met this in Chapter 1: a computer stores a number in a fixed number of bits, and some decimal values don’t fit exactly, so the answer lands a hair off. That’s called
**roundoff error**, and it's not a bug in your code. It's themachine telling the truth about what it can represent.
We’ll come back to why when we get to binary. For now the practical consequence is enough: doctests on floats are fragile.
Three things doctests handle badly:
Floating-point results, for the reason above
Anything random, since the answer changes every run
Anything that depends on the current date or time
None of these mean “don’t test.” They mean the test has to be written differently. We’ll see how in a later interlude on unit testing.
Debugging#
What a doctest can and cannot catch#
You’ve met three kinds of error so far. They fail in three different ways, and a doctest is only useful against one of them.
Kind |
What happens |
Can a doctest catch it? |
|---|---|---|
Syntax error |
The code breaks Python’s grammar and never runs at all |
No. The cell fails before any test exists |
Run-time error |
The code runs, then blows up partway with a traceback |
Sort of. You get the traceback instead of a pass/fail |
Semantic error |
The code runs happily and gives the wrong answer |
Yes. This is the whole point |
The first two announce themselves. A semantic error does not: nothing turns red, nothing complains, and the program looks like it worked. That silence is why they’re the hardest to find, and it’s exactly the silence a doctest breaks.
A word about words. Downey calls this a semantic error. The AP exam calls the same thing a logic error. Both mean “it ran, and it was wrong.”
There are other kinds of error you’ll meet later, when we look at how numbers are actually stored: roundoff error, which you saw above, and overflow error, which happens when a value is too large for the bits available to hold it. Python hides overflow from you almost completely, so it needs its own demonstration. Both belong to a later interlude on binary, not to this one.
Which one is lying, the code or the test?#
When a doctest fails, one of two things is wrong: the code, or the test. Beginners assume it’s always the code. It isn’t.
Before changing anything, do a hand trace: work through the call on paper, line by line, writing down what each variable holds as you go. No running the code, no guessing. The exam will ask you to do this with nothing but a pencil, so it’s worth practicing while you still have a machine to check yourself against.
Then compare your traced answer to both the Expected and the Got.
If your traced answer matches Expected, the code is wrong. Fix the function.
If your traced answer matches Got, the test is wrong. Fix the docstring.
If it matches neither, you don’t yet understand the problem, and changing code at random will not help. Go back to the description.
That third case is the common one, and it’s the whole reason to trace before you type.
Glossary#
docstring: A string at the beginning of a function that documents what the function does; unlike a comment, it is stored on the function and can be read by the program. (Python’s name for what the exam calls program documentation.)
doctest: An example call and its expected result, written inside a docstring, that can be run automatically to check the function. (Python; not exam vocabulary.)
testing: Checking that a program behaves correctly by running it on chosen inputs and comparing what comes out against what should have come out.
test case: A single input, paired with the result it should produce.
boundary case: A test case at the value where a function’s behavior changes, such as zero, an empty string, or the first or last item.
edge case: A test case at an unusual or extreme input, where a function is most likely to be wrong.
expected value: What a test says the answer should be, as opposed to what the code actually produced.
pass: A test whose actual result matches its expected value.
fail: A test whose actual result does not match its expected value.
hand tracing: Working through code on paper line by line, writing down each variable’s value, in order to find an error without running the program.
semantic error: An error that lets the program run but produces a wrong result. (The exam calls this a logic error.)
roundoff error: A loss of precision that happens because a fixed number of bits cannot represent some numbers exactly.
program purpose: The need a program serves, or the problem it solves; why it exists.
program function: What a program does when it runs, described as behavior.
program input: Data a program receives while it is running.
program output: What a program produces: displayed text, a returned value, a file, a sound, a movement.
procedure: The exam’s word for a named, reusable block of code, whether or not it returns a value. This book says function. In older languages the two words were distinct: a procedure returned nothing, a function returned a value.
regression: A bug that reappears in code that used to work. Tests exist mainly to catch these. (Professional vocabulary, not exam vocabulary.)
See also: the full vocabulary glossary collects every term in this book, alphabetized, alongside the complete AP CSP exam vocabulary list.
Homework#
Do these in order. Exercises 1 through 5 are required.
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
Exercise 1#
Below is is_leap_year from Chapter 5, correct but undocumented. Add a docstring with a
summary line and three doctests: one typical year, one boundary case, and one case
that would catch a wrong implementation. Then run them.
(A hint on the boundary: 2000 and 1900 are both interesting, for different reasons.)
def is_leap_year(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
run_doctests(is_leap_year)
Exercise 2 (choose one: do Exercise 2 or Exercise 3, not both)#
Write a function can_ride(height_inches, age) that returns True if a rider is at least
48 inches tall and at least 7 years old. Write the docstring first, including three
doctests, then write the body. One of your three should be a boundary case.
Then, in the markdown cell below, state the function’s purpose in one sentence. Say something the docstring does not say.
# Your function here
run_doctests(can_ride)
Purpose, one sentence:
Exercise 3 (choose one: see Exercise 2)#
Write a function letter_grade(score) that returns 'A' for 90 and above, 'B' for 80
to 89, 'C' for 70 to 79, 'D' for 60 to 69, and 'F' below 60. Write the docstring
first, including three doctests, then write the body. One of your three should be a
boundary case.
Then, in the markdown cell below, state the function’s purpose in one sentence. Say something the docstring does not say.
# Your function here
run_doctests(letter_grade)
Purpose, one sentence:
Exercise 4#
The function below is broken. Its doctests are correct. Run them, read the failure report, and fix the function so all tests pass. Do not change the docstring.
Before you fix it, hand trace count_down_to_zero(3) and name which kind of error this
is: syntax, run-time, or semantic.
def count_down_to_zero(n):
"""Return a list of numbers from n down to 0.
>>> count_down_to_zero(3)
[3, 2, 1, 0]
>>> count_down_to_zero(0)
[0]
"""
result = []
while n > 0:
result.append(n)
n = n - 1
return result
run_doctests(count_down_to_zero)
Exercise 5#
Here a function and its doctest disagree. Exactly one of them is wrong.
Run the test. Then, in the markdown cell below, say which one is wrong and how you know. Then fix that one, and only that one.
def middle_character(word):
"""Return the middle character of a word with an odd number of letters.
>>> middle_character('cat')
'a'
>>> middle_character('hello')
'e'
"""
return word[len(word) // 2]
run_doctests(middle_character)
Type your answer here: which is wrong, the code or the test, and how do you know?
Exercise 6#
Reflection, three or four sentences.
You wrote docstrings in Chapter 4 without any way to check them. Now you can check them. Did knowing the test would run change how you wrote the docstring? Was there a moment where writing the examples first made the function easier to write, or harder?
Type your answer here.
Write a test that lies (extra credit)#
Write a function that is completely correct, along with a doctest that fails anyway. Then explain in a sentence why it fails.
There is more than one way to do this, and the chapter mentions three of them.
# Your function here
Type your explanation here.
from working_in_python import time_check
chapter_minutes = 0
extra_exercises_minutes = 0
longest = 0
time_check(chapter_minutes, extra_exercises_minutes, longest)
Finished? Copy your work#
This isn’t part of the exercises above. It’s a tool. Run the cell below to copy this notebook (including anything you’ve run) so you can paste it into a document.
working_in_python.show_copy_notebook_button()
Working in Python — modified by Eric Brown for a high school Computer Science Principles class. Source and modifications: github.com/porttack/working-in-python
Copyright 2024 Allen B. Downey
Code license: MIT License
Text license: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Standards alignment#
AP CSP: 1.3 Program Design and Development, 1.4 Identifying and Correcting Errors. Big Idea 1, 10-13% of the exam California 9-12: 9-12.AP.20, 9-12.AP.22 CSTA 2026: HS-PRO-TR-19 CA CTE (ICT): C4.9, C5.4
The exam asks you to choose inputs and the outputs they should produce, then use the results to find errors. A doctest is exactly that, written down. On the Create Performance Task you’ll be asked to describe two calls to a procedure you wrote, what each one tests, and what each returns. That is the same work you’re doing here, in the same order.
This interlude also names the three error types the exam distinguishes (syntax, run-time, and semantic, which the exam calls logic), and introduces the purpose / function / input / output frame that the written-response section asks you to fill in. Overflow and roundoff errors are named here but explained in a later interlude on binary, where the reason for them actually lives.
Syntax note: docstrings and >>> are Python, not exam pseudocode. The exam has no
notation for documentation or testing. What transfers is the habit, not the punctuation.