This lesson defines a Time class, introduces pure functions and the prototype-and-patch development plan, and works through the carrying problem that a naive time addition runs into.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 16 — Classes and functions
§16.1-16.2, pp. 155-156
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §16.1-16.2, pp. 155-156 — the pages these objectives are drawn from
Warm-up
You can do this in your head. Watch what you do.
Discussion prompt
A film starts at 9:45 and runs for 1 hour 35 minutes. Work out when it finishes, and describe the step you performed that plain column addition would not.
Hint: 45 plus 35 is 80.
Answer:
Adding the columns gives 10 hours and 80 minutes. Eighty minutes is not a valid number of minutes, so you carried: sixty of them became an hour, leaving 11:20.
That carrying step is what makes time arithmetic more than addition, and it is what a naive program leaves out.
This lesson writes exactly that naive program, watches it print 10:80:00, and patches it — which is a development plan with a name.
Concept
In the next few sections we'll write two functions that add time values. They demonstrate two kinds of function — pure functions and modifiers — and a development plan called prototype and patch, which is a way of tackling a complex problem by starting with a simple prototype and incrementally dealing with the complications.
prototype and patch — A development plan that involves writing a rough draft of a program, testing, and correcting errors as they are found.
The plan is worth naming because it has a real alternative, which the chapter comes to later — and because knowing you are following it makes its weaknesses easier to watch for.
Figure (svg): A pipeline showing a prototype tested, patched, and tested again
Think Python, 2nd edition — Allen B. Downey §16.1-16.2, pp. 156-156
Section
Section 1
Concept
As another example of a programmer-defined type, we'll define a class called Time that records the time of day.
class Time:
"""Represents the time of day.
attributes: hour, minute, second
"""
time = Time()
time.hour = 11
time.minute = 59
time.second = 30| Part | What it does | Note |
|---|---|---|
| the class | a docstring listing the attributes | as for Rectangle |
| Time() | an instance with no attributes yet | |
| three assignments | create them | 11:59:30 |
This is exactly the style of chapter 15: the class body is a docstring, and every attribute comes into being when it is assigned. The docstring's list is documentation that nothing enforces.
Think Python, 2nd edition — Allen B. Downey §16.1-16.2, pp. 155-155
Picture it
One object, three attributes, all numbers.
Figure (svg): An object diagram showing a Time instance with hour, minute and second attributes
No embedded object means copy.copy would be enough here — which is a small reward for a simple design, and a reminder that the previous lesson's problem came from nesting.
Worked example
The exercise, and the format sequence that makes it work.
def print_time(t):
print('%.2d:%.2d:%.2d' % (t.hour, t.minute, t.second))
# 9:45:0 without the .2
# 09:45:00 with it| Sequence | What it produces | Note |
|---|---|---|
| %d | an integer | '9' |
| %.2d | at least two digits | '09' |
| three of them | hour, minute, second | colon-separated |
Use the hint.
Why: The format sequence '%.2d' prints an integer using at least two digits, including a leading zero if necessary.
Build the whole line as one string.
Why: Three sequences and a tuple of three values, matched in order — lesson 14a's format operator.
Compare with plain %d.
Why: Nine o'clock would print as 9:45:0, which is legible and not the conventional form.
Figure (svg): The state of the program after each line of Worked example printing with leading zeros, drawn as a ladder with one rung per traced line
A time printed as hour:minute:second with leading zeros. The .2 is the only difference from a plain integer format.
Verify: Test it on a time with a small second value.
Why: A time of 9:45:00 must print as 09:45:00 rather than 9:45:0 — and choosing a case with a zero second is what exercises the padding. A test with 11:59:30 would pass either way and prove nothing.
Prediction
A single-digit number.
print('%.2d' % 9)| Sequence | What it gives | Note |
|---|---|---|
| %d | would give '9' | one digit |
| %.2d | at least two digits | '09' |
| the padding | a leading zero | when needed |
Predict first
What does this print?
Correct: 09 — the format sequence prints an integer using at least two digits, including a leading zero if necessary.
Why: The .2 is a minimum width rather than a decimal place, which is why it looks like a float format and is not one. A two-digit number is unaffected: '%.2d' % 45 gives '45'. That is what makes the sequence right for a clock, where every field is padded to two digits.
Worked example
The book's challenge, and the trick is the comparison rule from chapter 12.
def is_after(t1, t2):
return (t1.hour, t1.minute, t1.second) > (t2.hour, t2.minute, t2.second)| Part | What it does | Note |
|---|---|---|
| the tuples | hour first, then minute, then second | most significant first |
| the comparison | element by element | stops at the first difference |
| the result | a boolean | returned directly |
Recall the tuple comparison rule.
Why: Python compares the first element from each sequence and moves on only while they are equal — lesson 12a.
Order the fields by significance.
Why: Hour first, because a later hour settles the question whatever the minutes are.
Return the comparison directly.
Why: It is already a boolean, so no if statement is needed — which is the challenge.
Figure (svg): Two time tuples compared element by element with the deciding position marked
True if t1 follows t2 chronologically. The tuple comparison does all the work, because times and tuples are ordered the same way.
Verify: Check the equal case.
Why: Two identical times give False, since > is strict — which is right for is after. Testing equality explicitly matters because it is the boundary case the comparison rule handles by running out of elements rather than by finding a difference.
Trap
A student writes if t1.hour > t2.hour: return True, then elif t1.hour == t2.hour: and nests further.
Handle each field in turn
Why: Which is the correct algorithm, spelled out.
It takes a dozen lines with three levels of nesting, and every one of them is an opportunity for an inverted comparison — while a tuple comparison does the same thing in one line.
Build tuples and compare them.
(t1.hour, t1.minute, t1.second) > (...)
Why: Ordered most significant first.
The comparison rule does the nesting for you
Why: Element by element, stopping at the first difference.
This is chapter 12's rule earning its place. Any comparison with a priority order between fields is a tuple comparison, and writing the nesting by hand is doing something the language already does correctly.
Faded example
Three padded fields, colon-separated.
Fill in the blanks
def print_time(t):
print('%.2d:%.2d:%.2d' % (t.hour, t.minute, t.second))
Why: The three format sequences are matched with the three tuple elements in order, so the third must be the seconds. Each %.2d pads to at least two digits, which gives the conventional clock form — 09:45:00 rather than 9:45:0.
Discrimination
Tuples compare element by element, stopping at the first difference.
Sort into buckets
For each pair of times, which field settles the comparison?
Explain it to yourself
It looks like a coincidence and is not.
Discussion prompt
Comparing (hour, minute, second) tuples happens to order times correctly. Why is that not a coincidence?
Hint: What does each field's position mean?
Answer:
Because a time is written most significant field first, and tuples compare most significant position first. The two orderings are the same ordering.
A later hour makes a time later regardless of the minutes, which is exactly the rule that the first difference decides the comparison and nothing after it is consulted.
It is the same reason alphabetical order works letter by letter, and the same reason dates written year-month-day sort correctly as text. Putting the dominant field first is what makes a simple left-to-right scan give the right answer.
Section
Section 2
Concept
add_time creates a new Time object, initialises its attributes, and returns a reference to the new object. This is called a pure function because it does not modify any of the objects passed to it as arguments, and it has no effect — like displaying a value or getting user input — other than returning a value.
pure function — A function that does not modify any of the objects it receives as arguments. Most pure functions are fruitful.
def add_time(t1, t2):
sum = Time()
sum.hour = t1.hour + t2.hour
sum.minute = t1.minute + t2.minute
sum.second = t1.second + t2.second
return sum| Line | What it does | Note |
|---|---|---|
| sum = Time() | a new object | not one of the arguments |
| the assignments | fill it in | reading t1 and t2 |
| return sum | the only effect | nothing is displayed or modified |
The second condition is the one people forget: no effect other than returning a value. A function that modifies nothing and prints something is not pure.
Think Python, 2nd edition — Allen B. Downey §16.1-16.2, pp. 156-156
Picture it
Two conditions, and failing either one is enough.
Figure (svg): Two columns separating what a pure function may do from what it may not
Which makes print_time impure — it modifies nothing and its whole purpose is an effect.
Worked example
Four functions from this chapter and the last.
add_time(t1, t2) # builds and returns: pure
print_time(t) # displays: not pure
is_after(t1, t2) # reads and returns a boolean: pure
increment(t, secs) # modifies its argument: not pure| Function | What it does | Pure? |
|---|---|---|
| add_time | creates a new Time | modifies nothing |
| print_time | displays a value | an effect |
| is_after | reads and compares | pure |
| increment | modifies its argument | a modifier |
Check the first condition.
Why: Does it modify any object passed as an argument? add_time and is_after do not; increment does.
Check the second.
Why: Does it have any effect other than returning a value? print_time displays one, which disqualifies it however little it modifies.
Note that pure does not mean useful-only.
Why: A program made entirely of pure functions could compute anything and show you nothing, so impure functions have to exist somewhere.
Figure (svg): Two columns sorting four chapter functions into pure and impure
Two pure and two not, for two different reasons. The impurity of print_time is deliberate — displaying is its entire job.
Verify: Ask what a program of only pure functions could do.
Why: Compute, and nothing else — no output, no input, no files. So the goal is never total purity but keeping the effects in a few identifiable places, which is what the book means by a functional programming style.
Sorting
Check both conditions.
Sort into buckets
For each function description, is it pure?
Worked example
One line is what makes add_time pure.
def add_time(t1, t2):
sum = Time() # the line that makes it pure
sum.hour = t1.hour + t2.hour
...
return sum
# without it - sum = t1 - the function would modify
# the caller's Time and return it| First line | What follows | Note |
|---|---|---|
| sum = Time() | a new object | pure |
| sum = t1 | an alias for the argument | a modifier in disguise |
| the return value | looks the same either way | which hides the difference |
Notice what the first line establishes.
Why: Everything after it assigns to a new object, so no argument can be affected.
Consider the alternative.
Why: sum = t1 would make every following assignment modify the caller's Time, and the function would still return something that looked right.
Note that the caller cannot tell.
Why: The return value is identical in both cases; only the caller's original object differs.
Figure (svg): The state of the program after each line of Worked example why the new object matters, drawn as a ladder with one rung per traced line
The purity lives in one line. Changing it to an alias would produce a function that works, returns the right answer, and destroys its argument.
Verify: Check the argument after calling.
Why: start.minute is still 45 after add_time, which confirms nothing was modified — and it is worth checking with is as well: the returned object is not the same object as either argument. That comparison is the only reliable test, since the values alone would not distinguish the two versions.
Trap
A student calls a function pure because it has a return statement and produces the right answer.
Judge by the return value
Why: Fruitful functions look like the pure ones in the examples.
Purity is about what else the function does. A function can return a correct value and still modify its argument or print something, and either disqualifies it.
Check both conditions.
Does it modify any argument?
Why: If so, it is a modifier.
Does it do anything other than return?
Why: Printing, reading input, writing a file — any of these.
Most pure functions are fruitful, which is why the two get confused. But returning a value is neither necessary nor sufficient: the test is what the function does besides.
Prediction
add_time builds a new object.
start.minute = 45
done = add_time(start, duration)
print(start.minute)| Step | What happens | Result |
|---|---|---|
| sum = Time() | a new object | inside the function |
| the assignments | to sum, not to start | |
| start.minute | unchanged | 45 |
Predict first
What does this print?
Correct: 45 — add_time is pure, so it does not modify any of the objects passed to it.
Why: The function creates its own Time on the first line and assigns only to that, so both arguments are read and left alone. Had the first line been sum = t1 instead, every assignment would have modified start and this would print 80 — with the function still returning something that looked correct.
Two truths and a lie
Two are true. Keep the lie.
Eliminate the wrong options
Rule out the two true statements.
Survives elimination: C
Why: C confuses purity with fruitfulness. A function can return a correct value and still modify its argument or print something — either of which makes it impure. The book notes that most pure functions are fruitful, which is a statement about what is common rather than a definition.
Faded example
The first line decides it.
Fill in the blanks
def add_time(t1, t2):
sum = Time()
sum.hour = t1.hour + t2.hour
sum.minute = t1.minute + t2.minute
sum.second = t1.second + t2.second
return sum
Why: Creating a new object means every assignment that follows touches something the caller does not hold, so neither argument is modified. Writing sum = t1 instead would alias the first argument and turn every later line into a modification of the caller's Time — while still returning something that looked right.
Section
Section 3
Concept
To test the function, the book creates two Times: start contains the start time of a film, and duration contains its run time of one hour thirty-five minutes. add_time figures out when the film will be done.
>>> start.hour = 9
>>> start.minute = 45
>>> duration.hour = 1
>>> duration.minute = 35
>>> done = add_time(start, duration)
>>> print_time(done)
10:80:00| Column | The addition | Result |
|---|---|---|
| 9 + 1 | the hours | 10 |
| 45 + 35 | the minutes | 80 |
| the result | 10:80:00 | not a time |
The result might not be what you were hoping for. The problem is that this function does not deal with cases where the number of seconds or minutes adds up to more than sixty — when that happens, we have to carry.
Think Python, 2nd edition — Allen B. Downey §16.1-16.2, pp. 156-156
Picture it
Both columns were added correctly, and one of them cannot hold its answer.
Figure (svg): A ladder showing the hour and minute columns added independently with the minute column overflowing
Which is what makes this such a good example: the program is doing addition correctly and producing something that is not a time.
Worked example
Nothing about 80 is illegal to Python.
sum.minute = t1.minute + t2.minute # 80
# 80 is a perfectly good integer.
# Nothing in the Time class says a minute
# must be less than 60 - the class body
# is a docstring.| Aspect | Status | Note |
|---|---|---|
| the arithmetic | correct | 45 + 35 = 80 |
| the assignment | legal | any integer may be stored |
| the meaning | wrong | and nothing checks meaning |
Check the arithmetic.
Why: It is right: forty-five plus thirty-five is eighty.
Check the assignment.
Why: Also legal — an attribute can hold any value, and the class declares nothing about ranges.
Locate the error.
Why: It is semantic: the program does exactly what it says and what it says is not what was meant.
Figure (svg): A panel showing that the incorrect result raised no error at any stage
A semantic error, of the kind chapter 1 named. No message appears because nothing illegal happened — the meaning is wrong and the mechanics are fine.
Verify: Ask what would have caught it.
Why: A check that the result is a valid Time, which is exactly the valid_time function the next lesson introduces as an invariant check. Nothing in the language will notice, so noticing has to be something the program does deliberately.
Prediction
No carrying yet.
# start 9:45:00, duration 1:35:00
done = add_time(start, duration)
print_time(done)| Column | The addition | Result |
|---|---|---|
| hours | 9 + 1 | 10 |
| minutes | 45 + 35 | 80 |
| no carry | the prototype has none | 10:80:00 |
Predict first
What does this print?
Correct: 10:80:00 — the prototype adds the columns and does not carry.
Why: Every step is legal: the arithmetic is right and an attribute may hold any integer, so nothing raises. The correct answer, 11:20:00, needs the carry the patched version adds. This is a semantic error, which is why it announces itself only in the output.
Worked example
Two conditionals, one per column boundary.
def add_time(t1, t2):
sum = Time()
sum.hour = t1.hour + t2.hour
sum.minute = t1.minute + t2.minute
sum.second = t1.second + t2.second
if sum.second >= 60:
sum.second -= 60
sum.minute += 1
if sum.minute >= 60:
sum.minute -= 60
sum.hour += 1
return sum| Part | What it does | Note |
|---|---|---|
| the additions | as before | may overflow |
| the first if | carries seconds into minutes | if needed |
| the second if | carries minutes into hours | after the first |
Do the basic addition.
Why: The first three lines are unchanged from the prototype.
Carry the seconds.
Why: If the seconds reached sixty, subtract sixty and add one to the minutes.
Carry the minutes, after that.
Why: The order matters: the seconds carry may push the minutes to sixty, so the minute check has to come second.
Figure (svg): The state of the program after each line of Worked example the patched version, drawn as a ladder with one rung per traced line
11:20:00 for the film example. Although this function is correct, it is starting to get big — which is the book's own verdict and sets up the rewrite later in the chapter.
Verify: Test a case where both carries fire.
Why: Adding 0:59:59 to 0:00:01 gives seconds of 60, which carries to minutes of 60, which carries to an hour — so the answer is 1:00:00. That case exercises the ordering, and reversing the two conditionals would produce 0:60:00 instead.
Trap
The minute carry is written before the second carry, since minutes feel more significant.
Handle the bigger unit first
Why: Which is how you would read the time aloud.
The seconds carry can push the minutes to sixty, and by then the minute check has already run. Adding 0:59:59 and 0:00:01 gives 0:60:00 — legal, wrong, and silent.
Carry from the smallest column upward.
Seconds first, then minutes
Why: So that a carry produced by the first check is seen by the second.
And test the case where both fire
Why: Which is the only case that distinguishes the two orderings.
This is ordinary column addition: you carry rightmost first, for exactly this reason. Getting it wrong produces correct answers for almost every input, which is what makes the test case worth constructing deliberately.
Invariant
The case where both conditionals fire.
Step through it
What would the answer be if the two conditionals were swapped?
0:60:00 — the minute check would run before the seconds carry created the problem, so it would see 59 and do nothing. Carrying from the smallest column upward is what makes the second check see the first one's effect.
Faded example
Subtract sixty, and add one to the next column.
Fill in the blanks
if sum.second >= 60:
sum.second -= 60
sum.minute += 1
Why: Sixty seconds make a minute, so the carry goes into the minute column. And this check must come before the minute check, since the carry it performs can push the minutes to sixty — which is what the next conditional then has to catch.
Explain it
A wrong answer with no error message.
Discussion prompt
A classmate's time addition printed 10:80:00 and they want to know why Python did not object. Explain.
Hint: Which step was illegal?
Answer:
None of them. The arithmetic is correct, and an attribute can hold any integer — the Time class body is a docstring, which declares nothing about valid ranges.
So this is a semantic error: the program does exactly what it says, and what it says is not what was meant. Those never produce a message, which is what makes them the hardest kind to find.
The only thing that would catch it is a check the program performs itself — a function that tests whether a Time is valid. That is exactly what the next lesson's valid_time does, and it is why invariants are worth writing down.
Section
Section 4
Concept
The plan is to write a prototype that performs the basic calculation, test it, and patch the errors along the way — tackling a complex problem by starting with a simple prototype and incrementally dealing with the complications.
The last clause is the weakness, and the chapter comes back to it: incremental corrections can generate code that is unnecessarily complicated, and unreliable, since it is hard to know if you have found all the errors.
Think Python, 2nd edition — Allen B. Downey §16.1-16.2, pp. 156-156
Picture it
Each pass adds a special case.
Figure (svg): A flowchart showing the prototype-and-patch cycle accumulating special cases
add_time has been round it once already, and the book's verdict on the result is that it is starting to get big.
Worked example
It works, and it works best in a particular situation.
# pass 1: the basic calculation
sum.minute = t1.minute + t2.minute
# pass 2: patched, after seeing 10:80:00
if sum.minute >= 60:
sum.minute -= 60
sum.hour += 1| Stage | What it gives you | Note |
|---|---|---|
| the first pass | something that runs | quickly |
| the test | shows the problem concretely | 10:80:00 |
| the patch | addresses what was seen | narrow and specific |
Note what you get early.
Why: Something that runs, which can be tested — and the test showed the problem far more clearly than thinking about it would have.
Note when this is most valuable.
Why: This approach can be effective, especially if you don't yet have a deep understanding of the problem.
Note what the patch is based on.
Why: An observed failure rather than an anticipated one, which is why it is exactly the right size.
Figure (svg): The state of the program after each line of Worked example where the plan is strong, drawn as a ladder with one rung per traced line
A working program quickly, and evidence about what is actually hard. The 10:80:00 result taught more in one line of output than a paragraph of reasoning would have.
Verify: Ask what the alternative would have cost at this stage.
Why: Understanding the problem well enough to design a solution — which for time arithmetic means noticing it is base 60, an insight the book does not reveal until §16.4. Waiting for that insight before writing anything would mean writing nothing for a while, and there is no guarantee it arrives.
Prediction
The patched add_time carries once per column.
# both arguments are valid Times:
# each minute is 0..59
# so their sum is at most 118
# one subtraction of 60 leaves at most 58| Quantity | Its range | Note |
|---|---|---|
| each field | under 60 | in a valid Time |
| the sum | under 120 | at most 118 |
| one carry | leaves under 60 | sufficient |
Predict first
Is one carry per column enough for add_time?
Correct: Yes — each field of a valid Time is under 60, so the sum is under 120 and one subtraction of 60 is enough.
Why: That argument is what makes the function correct rather than merely untested. It also shows why the argument matters: the next lesson's increment adds an arbitrary number of seconds, where the same two conditionals are not enough — and the difference is exactly this range reasoning.
Worked example
The book's two criticisms, and both are visible already.
def add_time(t1, t2):
sum = Time()
sum.hour = t1.hour + t2.hour
sum.minute = t1.minute + t2.minute
sum.second = t1.second + t2.second
if sum.second >= 60:
sum.second -= 60
sum.minute += 1
if sum.minute >= 60:
sum.minute -= 60
sum.hour += 1
return sum
# 'Although this function is correct,
# it is starting to get big.'| Criticism | Evidence here | Note |
|---|---|---|
| complicated | two special cases already | for one operation |
| unreliable | are there more? | hard to know |
| the book's verdict | starting to get big | after one patch |
Note the first criticism.
Why: Incremental corrections can generate code that is unnecessarily complicated, since it deals with many special cases — and there are two already.
Note the second.
Why: And unreliable, since it is hard to know if you have found all the errors.
Ask the uncomfortable question.
Why: Is this version correct for every input? The two conditionals carry once each, which is enough here — and the next lesson's increment shows a case where carrying once is not.
Figure (svg): Two columns weighing the strengths of prototype-and-patch against its weaknesses
Correct, and growing, with no way to know whether it is finished. Both of the book's criticisms are already visible after a single patch.
Verify: Look for an input the two carries cannot handle.
Why: add_time is safe, because each field of a valid Time is under sixty, so the sum of two is under 120 and one carry suffices. That argument is worth making explicitly — it is the difference between no test has failed and it cannot fail, and only the second is a reason to stop patching.
Trap
A patched function passes every test tried, so it is considered finished.
Take passing tests as correctness
Why: It is the evidence available, and it is real evidence.
It shows that the errors you found are fixed, not that there are none left — which is exactly the book's second criticism. Untested inputs are the ones the special cases were not written for.
Argue that no more cases exist, or accept the uncertainty.
Reason about the range of inputs
Why: One carry suffices for add_time because each field is under sixty.
Or switch to designed development
Why: Which removes special cases rather than testing them.
Passing tests and being correct are different claims. Prototype and patch produces the first and only reasoning produces the second, which is why the chapter goes on to a plan that avoids the special cases entirely.
Comparison
Fill the blanks. One patch, and both costs already visible.
Comparison matrix
| Question | The prototype | After the patch |
|---|---|---|
| How many lines? | six | twelve |
| Correct? | no — 10:80:00 | yes, for valid Times |
| Special cases | none | two, one per column boundary |
| The book's verdict | does not deal with carrying | correct, but starting to get big |
Doubling in length to handle one complication is the pattern the plan produces, and it is why the chapter looks for an alternative.
Ranking
Four steps, in order.
Put in order
Why: Write, test, patch, repeat. The order matters because the patch is based on an observed failure rather than an anticipated one — which is what makes it exactly the right size, and also why the loop's exit condition is only nothing I have found.
Real world
It is recommended, with reservations.
Discussion prompt
Think of a situation where building something rough first was clearly the right move. What did the rough version teach you?
Hint: What did you not know at the start?
Answer:
A first draft, a mock-up, a trial run — in each case what you learn is which parts are actually hard, which is usually not what you predicted.
The book's own condition is this: the approach can be effective, especially if you don't yet have a deep understanding of the problem. The prototype is a way of acquiring that understanding cheaply.
What it is not good for is arriving at something clean. The accumulated special cases are the record of everything you learned, in the least organised possible form — which is why the sensible move is often to rewrite once you understand, rather than patching indefinitely.
Section
Section 5
Concept
The prototype was tested with a film starting at 9:45 and running for 1 hour 35 minutes — which is not an arbitrary choice, because it is a case where the minutes overflow.
# a test that proves nothing:
# 9:00 + 1:00 = 10:00 no carrying needed
# the test the book uses:
# 9:45 + 1:35 = 10:80 the carry is exposed| Test | What it shows | Note |
|---|---|---|
| a round-numbers test | passes either version | proves nothing |
| a test that overflows | distinguishes them | exposes the bug |
| choosing the input | the actual skill |
A test that passes on both the broken and the fixed version has told you nothing. Choosing inputs that could distinguish them is where the work is.
Python documentation — Classes Classes
Picture it
Most inputs cannot tell you anything.
Figure (svg): Two columns separating tests that expose the carrying bug from tests that cannot
Which is a general principle: a test that both a correct and an incorrect program pass is not a test of the thing you care about.
Worked example
A test that does not need a hand-computed answer.
# for any valid Time t and any n:
# adding n seconds n times, one at a time,
# should equal adding them all at once
# and: a result must be a valid Time
# 0 <= minute < 60, 0 <= second < 60| Technique | What it checks | Note |
|---|---|---|
| two routes to one answer | compare them | a consistency check |
| a validity check | the result must be a Time | an invariant |
| neither needs | a hand-computed expected value |
Find a second route to the same answer.
Why: Adding an hour to a time twice should equal adding two hours once, whatever the time — which is checkable for many inputs without working any of them out.
Check the result is well formed.
Why: Every field within range, which the 10:80:00 result would have failed immediately.
Note what this buys.
Why: Both can run over many inputs automatically, where hand-computed expected values must be worked out one at a time.
Figure (svg): A flowchart showing a computation followed by a validity check on the resulting Time
Two checks that need no expected answers, so they can be run over hundreds of inputs. This is lesson 11c's consistency and sanity checking, applied to a class.
Verify: Run the validity check against the prototype.
Why: It fails on 9:45 + 1:35, reporting a minute of 80 — which is the bug found automatically rather than by looking at output. That is the difference between a check the program performs and a person reading a result, and the next lesson formalises it as valid_time.
Discrimination
The prototype carries nothing; the patched version carries once per column.
Sort into buckets
For each input, would the prototype and the patched add_time disagree?
Worked example
Both carries have to fire.
# 0:59:59 + 0:00:01
# seconds: 60 -> carry -> minutes 60
# minutes: 60 -> carry -> hours 1
# correct answer: 1:00:00
# with the conditionals swapped: 0:60:00| Input | What it distinguishes | Note |
|---|---|---|
| one carry only | either ordering works | no information |
| both carries | the orderings disagree | informative |
| the boundary | exactly 60 | worth testing deliberately |
Identify what the ordering affects.
Why: Only a case where the seconds carry pushes the minutes to sixty — otherwise both orderings agree.
Construct that case.
Why: 59 minutes and 59 seconds plus one second, which is the smallest input that does it.
Note how specific it is.
Why: Almost every other input passes both versions, so this test has to be built deliberately rather than stumbled on.
Figure (svg): The state of the program after each line of Worked example the test that would have caught the ordering bug, drawn as a ladder with one rung per traced line
One input that distinguishes the two orderings, out of the vast majority that do not. Finding it requires thinking about what the code does rather than trying values.
Verify: Check the boundary itself.
Why: Sixty seconds exactly is the boundary, and >= 60 is correct where > 60 would leave a legal-looking 0:00:60 behind. Testing at the boundary rather than near it is what catches an off-by-one in the comparison, which is a different bug from the ordering one.
Trap
A time function is tested with 9:00 plus 1:00 and reported as working.
Use a simple case you can check by hand
Why: Which is good advice for a first test.
No column overflows, so the broken and fixed versions agree, and the test cannot distinguish them. It confirms only that the additions happen at all.
Choose inputs where the versions could disagree.
Something that overflows a column
Why: 9:45 + 1:35, as the book uses.
And the boundary exactly
Why: Sums of exactly 60, which catch off-by-one comparisons.
The question to ask of any test is what a wrong program would do on it. If the answer is the same thing, the test is checking that the program runs rather than that it is right.
Prediction
The two conditionals swapped.
# with the minute check written BEFORE the second check
# which input gives a wrong answer?| Case | What happens | Note |
|---|---|---|
| one carry only | both orderings agree | no difference |
| seconds carry into 60 minutes | the orderings differ | 0:60:00 vs 1:00:00 |
| the input | 0:59:59 + 0:00:01 |
Predict first
Which input would reveal that the conditionals are in the wrong order?
Correct: 0:59:59 + 0:00:01 — the seconds carry pushes the minutes to sixty, which only a later minute check would catch.
Why: The other three either need no carry at all or need only the minute carry, so both orderings agree on them. Only an input where the seconds carry creates the minute overflow distinguishes the two — which is why it has to be constructed deliberately rather than stumbled on.
Faded example
Every field must be in range.
Fill in the blanks
def valid_time(t):
if t.minute >= 60 or t.second >= 60:
return False
return True
Why: A minute or second of exactly sixty is already invalid, so the comparison must be >= rather than >. Using > would accept 10:60:00 as well formed, which is precisely the state a missing carry leaves behind — so the off-by-one would hide the bug the check exists to find.
Explain it
A common and demoralising situation.
Discussion prompt
A classmate's time function passes every test they wrote and produces a wrong answer in their program. Tell them what to look at.
Hint: What do their tests have in common?
Answer:
Their tests almost certainly avoid the hard cases. Round numbers, no overflow, nothing at a boundary — inputs a broken program handles as well as a correct one.
The question to ask of each test is what a wrong version would do on it. If the answer is the same thing, the test checks that the program runs rather than that it is right.
So the fix is to construct inputs deliberately: one per special case, one exactly at each boundary, and one where two special cases interact. That last kind is the one nobody stumbles on, and it is where the ordering bugs live.
Comparison
Fill the blanks. The chapter's two kinds of function.
Comparison matrix
| Question | Pure function | Modifier |
|---|---|---|
| Does it change its arguments? | no | yes — that is the definition |
| What does it usually return? | a new object | None |
| How is it called? | result = f(x) | f(x), as a statement |
| Which does add_time use? | this one — it builds a new Time | increment, in the next lesson |
Both appear in this chapter, and the book recommends preferring the first whenever it is reasonable.
Pattern
Five steps, and the fifth is the one that decides whether you are finished.
Step 2 is where most of the value is. A test that a broken version would also pass tells you the program runs, not that it is right.
Python documentation — Classes Classes
Check
Two conditions, and both are required.
Check your understanding
Which function is NOT pure?
Answer: A
Why: A pure function does not modify any of the objects passed to it and has no effect — like displaying a value or getting user input — other than returning a value. print_time modifies nothing and displaying is its entire purpose, which fails the second condition.
Check
No carrying.
Check your understanding
The prototype add_time is given 9:45:00 and 1:35:00. What does print_time show?
Answer: A
Why: The columns are added independently and nothing carries, so the minutes come to eighty. Every step is legal — the arithmetic is right and an attribute may hold any integer — so nothing raises. This is a semantic error, which announces itself only in the output.
Check
Two conditionals, and their order matters.
Check your understanding
Why must the seconds carry be checked before the minutes carry?
Answer: A
Why: The seconds carry adds one to the minutes, which can take them to sixty. If the minute check has already run, that overflow is never caught — so 0:59:59 plus 0:00:01 would give 0:60:00 rather than 1:00:00.
Real world
Building something rough to find out what is hard.
Discussion prompt
Think of something you built or planned in a rough version first. What did the rough version reveal that planning would not have?
Hint: What surprised you?
Answer:
Usually which part is actually difficult — and it is rarely the part that looked hardest in advance. A draft, a mock-up or a trial run answers that in a way thinking about it does not.
That is the book's own condition for the approach: it can be effective, especially if you don't yet have a deep understanding of the problem. The prototype is how you acquire the understanding.
What it does not give you is a clean result. The accumulated fixes are a record of everything you learned, in the least organised possible form — which is why rewriting once you understand is often better than patching indefinitely.
Commit first
Answer, then rate your confidence.
Predict first
A function returns a correct value and prints a progress message. Is it pure?
Correct: No — a pure function has no effect other than returning a value, and displaying one is an effect.
Why: The book's definition has two halves and this is the half people forget: a pure function does not modify any of the objects passed to it as arguments, AND it has no effect — like displaying a value or getting user input — other than returning a value. Modifying nothing satisfies the first condition and is not enough. This matters more than it sounds: the reason to prefer pure functions is that their behaviour is entirely captured by their return value, so you can reason about a call by looking at what came back. A function that also prints, writes a file, or reads input breaks that property, and the caller has to know about the effect as well as the result. The book's recommendation is to write pure functions whenever it is reasonable and resort to modifiers only if there is a compelling advantage.
Explain it
A wrong answer with no error message.
Discussion prompt
A classmate's time addition printed 10:80:00 and they cannot find the bug because nothing raised. Explain what kind of error this is and how to catch this family of them.
Hint: Which step was illegal?
Answer:
None of them was. The arithmetic is correct and an attribute may hold any integer — the class declares nothing about valid ranges, since its body is a docstring.
So it is a semantic error: the program does exactly what it says, and what it says is not what was meant. Those never produce a message, which is what makes them the hardest kind to find.
The way to catch them is a check the program performs on itself — a function testing whether a Time is well formed, run on every result. The next lesson formalises this as an invariant, and 10:80:00 would have failed it immediately.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
Correct: Whichever you picked is the right answer — this one is for you, not for a mark.
Why: The Time class is chapter 15's minimal style with three attributes, and the is_after exercise is worth doing because the tuple comparison makes it one line. The definition of pure has two halves and the second one — no effect other than returning a value — is the one people drop. The 10:80:00 result is the chapter's best teaching moment: a wrong answer with no error, which is exactly what a semantic error looks like. And the plan itself is worth naming, because knowing you are following it is what lets you notice its weaknesses in your own code.
Connect it up
One page, from memory.
Draw it
Draw the Time object diagram with its three attributes. Beside it, write the two conditions for a function to be pure, and sort four functions from this chapter into pure and not. Underneath, write the column addition of 9:45 and 1:35 showing where it overflows, and the two conditionals that patch it — marking why they must be in that order. Finally write down one test input that would distinguish the prototype from the patched version, and one that would not.
Recap
Two pages, and a development plan with a name.
| If you remember one thing | It is this |
|---|---|
| From the Time class | A time is three numbers, and comparing it is comparing a tuple. |
| From pure functions | Two conditions: no modification, and no effect besides returning. |
| From 10:80:00 | A semantic error: every step legal, and the meaning wrong. |
| From the patch | Carry upward, because the seconds carry can create a minute overflow. |
| From the plan | Passing tests means the errors you found are fixed, not that there are none. |
The next lesson writes the same operation as a modifier, weighs modifiers against pure functions, and then reaches the chapter's real payoff: the insight that a Time is a three-digit number in base 60, which replaces every special case with two conversion functions.
Think Python, 2nd edition — Allen B. Downey §16.1-16.2, pp. 155-156 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.