16a The Time Class and Pure Functions

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

What this lesson covers

The lesson, slide by slide

1. Lesson 16a The Time Class and Pure Functions

Title

Python · Chapter 16 — Classes and functions

§16.1-16.2, pp. 155-156

2. By the end of this lesson you can

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

3. Before we start: what time is 9:45 plus 1:35?

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.

4. The one idea behind this chapter: a plan for tackling a complex problem

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

Write, test, correct — and repeat until nothing more goes wrong that you can find.

Think Python, 2nd edition — Allen B. Downey §16.1-16.2, pp. 156-156

5. The Time class

Section

Section 1

6. Three attributes, in the minimal style

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
PartWhat it doesNote
the classa docstring listing the attributesas for Rectangle
Time()an instance with no attributes yet
three assignmentscreate them11: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

7. Picture it: figure 16.1, the object diagram

Picture it

One object, three attributes, all numbers.

Figure (svg): An object diagram showing a Time instance with hour, minute and second attributes

The book's figure 16.1. Simpler than the Rectangle, since nothing is embedded.

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.

8. Worked example: printing with leading zeros

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
SequenceWhat it producesNote
%dan integer'9'
%.2dat least two digits'09'
three of themhour, minute, secondcolon-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

The whole run at once: each drop is one line of the program.

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.

9. Predict: what does %.2d produce?

Prediction

A single-digit number.

print('%.2d' % 9)
SequenceWhat it givesNote
%dwould give '9'one digit
%.2dat least two digits'09'
the paddinga leading zerowhen needed

Predict first

What does this print?

  • 09
  • 9
  • 9.2
  • 0.9

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.

10. Worked example: is_after, without an if statement

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)
PartWhat it doesNote
the tupleshour first, then minute, then secondmost significant first
the comparisonelement by elementstops at the first difference
the resulta booleanreturned 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

The most significant field first, which is why a plain tuple comparison orders times correctly.

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.

11. Trap: comparing times field by field with nested ifs

Trap

The 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.

The fix

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.

12. Complete it: print a time

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.

13. Discriminate: which comparison decides it?

Discrimination

Tuples compare element by element, stopping at the first difference.

Sort into buckets

For each pair of times, which field settles the comparison?

the hour
11:59:30 against 10:00:00; 12:00:00 against 11:59:59
the minute
11:20:00 against 11:59:00; 10:30:00 against 10:45:00
the second
09:00:05 against 09:00:01; 08:15:20 against 08:15:19
h
The hours differ, so the comparison stops there and nothing later is examined — even a much larger minute value cannot change the answer.
m
The hours are equal, so the comparison moves on and the minutes settle it.
s
The hours and minutes both match, so the comparison reaches the last field.

14. Explain it yourself: why does a tuple comparison order times correctly?

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.

15. Pure functions

Section

Section 2

16. Two conditions, and both are required

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
LineWhat it doesNote
sum = Time()a new objectnot one of the arguments
the assignmentsfill it inreading t1 and t2
return sumthe only effectnothing 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

17. Picture it: what a pure function does and does not do

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

Reading is free; every kind of effect is not.

Which makes print_time impure — it modifies nothing and its whole purpose is an effect.

18. Worked example: which of these functions are pure?

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
FunctionWhat it doesPure?
add_timecreates a new Timemodifies nothing
print_timedisplays a valuean effect
is_afterreads and comparespure
incrementmodifies its argumenta 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 ways to be impure: change something, or do something visible.

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.

19. Sort: pure or not?

Sorting

Check both conditions.

Sort into buckets

For each function description, is it pure?

pure
returns the sum of two Times as a new Time; returns True if one Time is after another; converts a Time to an integer and returns it
not pure
prints a Time in hour:minute:second form; adds seconds to the Time it is given; asks the user for a time and returns it
pure
Each reads its arguments, computes, and returns — modifying nothing and doing nothing else. That is both conditions satisfied.
not
One modifies its argument; the other two have an effect other than returning a value — displaying, and getting user input, which the book names explicitly.

20. Worked example: why the new object matters

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 lineWhat followsNote
sum = Time()a new objectpure
sum = t1an alias for the argumenta modifier in disguise
the return valuelooks the same either waywhich 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 whole run at once: each drop is one line of the program.

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.

21. Trap: thinking a function is pure because it returns something

Trap

The 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.

The fix

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.

22. Predict: is the argument unchanged?

Prediction

add_time builds a new object.

start.minute = 45
done = add_time(start, duration)
print(start.minute)
StepWhat happensResult
sum = Time()a new objectinside the function
the assignmentsto sum, not to start
start.minuteunchanged45

Predict first

What does this print?

  • 45
  • 80
  • 0
  • None

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.

23. Two truths and a lie: pure functions

Two truths and a lie

Two are true. Keep the lie.

Eliminate the wrong options

Rule out the two true statements.

  • A. A pure function does not modify any of the objects passed to it as arguments
  • B. A pure function has no effect other than returning a value
  • C. Any function with a return statement is pure

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.

24. Complete it: keep the function pure

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.

25. The prototype, and what it gets wrong

Section

Section 3

26. 10:80:00

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
ColumnThe additionResult
9 + 1the hours10
45 + 35the minutes80
the result10:80:00not 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

27. Picture it: the column that overflowed

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

No arithmetic was wrong. The minute column simply holds a number it should not.

Which is what makes this such a good example: the program is doing addition correctly and producing something that is not a time.

28. Worked example: why no error was raised

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.
AspectStatusNote
the arithmeticcorrect45 + 35 = 80
the assignmentlegalany integer may be stored
the meaningwrongand 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.

29. Predict: what does the prototype print?

Prediction

No carrying yet.

# start 9:45:00, duration 1:35:00
done = add_time(start, duration)
print_time(done)
ColumnThe additionResult
hours9 + 110
minutes45 + 3580
no carrythe prototype has none10:80:00

Predict first

What does this print?

  • 10:80:00
  • 11:20:00
  • 10:20:00
  • An error about an invalid minute

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.

30. Worked example: the patched version

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
PartWhat it doesNote
the additionsas beforemay overflow
the first ifcarries seconds into minutesif needed
the second ifcarries minutes into hoursafter 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

The whole run at once: each drop is one line of the program.

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.

31. Trap: checking the columns in the wrong order

Trap

The 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.

The fix

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.

32. Watch the carries: 0:59:59 plus 0:00:01

Invariant

The case where both conditionals fire.

Step through it

What would the answer be if the two conditionals were swapped?

  1. The straight addition gives sixty seconds, which is not a valid second value.
  2. The first conditional carries: the seconds drop to zero and the minutes rise by one.
  3. The minutes are now sixty — a value that was valid a moment ago and is not any more.
  4. The second conditional carries again, giving 1:00:00. The result is correct only because the second check ran after the first.

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.

33. Complete it: carry the seconds

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.

34. Explain it: why did nothing warn me?

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.

35. Prototype and patch

Section

Section 4

36. The development plan being demonstrated

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

37. Picture it: the loop, and what it accumulates

Picture it

Each pass adds a special case.

Figure (svg): A flowchart showing the prototype-and-patch cycle accumulating special cases

Each trip round the loop leaves a special case behind, and the exit condition is nothing I have found.

add_time has been round it once already, and the book's verdict on the result is that it is starting to get big.

38. Worked example: where the plan is strong

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
StageWhat it gives youNote
the first passsomething that runsquickly
the testshows the problem concretely10:80:00
the patchaddresses what was seennarrow 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

The whole run at once: each drop is one line of the program.

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.

39. Predict: does one carry always suffice?

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
QuantityIts rangeNote
each fieldunder 60in a valid Time
the sumunder 120at most 118
one carryleaves under 60sufficient

Predict first

Is one carry per column enough for add_time?

  • Yes — each field of a valid Time is under 60, so the sum is under 120
  • No — the fields could add to any value
  • Only if the seconds are zero
  • Only for times under twelve hours

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.

40. Worked example: where it goes wrong

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.'
CriticismEvidence hereNote
complicatedtwo special cases alreadyfor one operation
unreliableare there more?hard to know
the book's verdictstarting to get bigafter 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

The book recommends it, and names both costs.

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.

41. Trap: stopping when the tests stop failing

Trap

The 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.

The fix

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.

42. Compare: the prototype and the patched version

Comparison

Fill the blanks. One patch, and both costs already visible.

Comparison matrix

QuestionThe prototypeAfter the patch
How many lines?sixtwelve
Correct?no — 10:80:00yes, for valid Times
Special casesnonetwo, one per column boundary
The book's verdictdoes not deal with carryingcorrect, 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.

43. Rank: the prototype-and-patch cycle

Ranking

Four steps, in order.

Put in order

  1. write a prototype doing the basic calculation
  2. test it on a case that exposes a complication
  3. patch the error by adding a special case
  4. test again, and repeat until nothing more fails

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.

44. Where prototype-and-patch is the right plan

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.

45. Testing a function you have just written

Section

Section 5

46. Choosing the case that would fail

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
TestWhat it showsNote
a round-numbers testpasses either versionproves nothing
a test that overflowsdistinguishes themexposes the bug
choosing the inputthe 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

47. Picture it: which inputs distinguish the versions

Picture it

Most inputs cannot tell you anything.

Figure (svg): Two columns separating tests that expose the carrying bug from tests that cannot

A test is only informative if the two versions could disagree on it.

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.

48. Worked example: a consistency check for time arithmetic

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
TechniqueWhat it checksNote
two routes to one answercompare thema consistency check
a validity checkthe result must be a Timean invariant
neither needsa 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

The check runs on every result, including the ones nobody reads.

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.

49. Discriminate: does this test distinguish the versions?

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?

they disagree — informative
9:45:00 + 1:35:00; 0:00:59 + 0:00:01; 0:59:00 + 0:01:00
they agree — proves nothing
9:00:00 + 1:00:00; 0:30:00 + 0:20:00; 1:10:00 + 2:20:00
yes
In each, some column sums to sixty or more, so the prototype leaves an out-of-range value and the patched version carries. Only these tell you which version you have.
no
In each, every column sums to less than sixty, so no carry is needed and both versions produce the same correct answer. Passing tells you nothing about the carrying.

50. Worked example: the test that would have caught the ordering bug

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
InputWhat it distinguishesNote
one carry onlyeither ordering worksno information
both carriesthe orderings disagreeinformative
the boundaryexactly 60worth 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

The whole run at once: each drop is one line of the program.

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.

51. Trap: testing with round numbers

Trap

The 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.

The fix

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.

52. Predict: which input exposes the ordering bug?

Prediction

The two conditionals swapped.

# with the minute check written BEFORE the second check
# which input gives a wrong answer?
CaseWhat happensNote
one carry onlyboth orderings agreeno difference
seconds carry into 60 minutesthe orderings differ0:60:00 vs 1:00:00
the input0:59:59 + 0:00:01

Predict first

Which input would reveal that the conditionals are in the wrong order?

  • 0:59:59 + 0:00:01
  • 9:45:00 + 1:35:00
  • 9:00:00 + 1:00:00
  • 0:00:30 + 0:00:20

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.

53. Complete it: a validity check

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.

54. Explain it: my tests all pass and it is still wrong

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.

55. Compare: a pure function and a modifier

Comparison

Fill the blanks. The chapter's two kinds of function.

Comparison matrix

QuestionPure functionModifier
Does it change its arguments?noyes — that is the definition
What does it usually return?a new objectNone
How is it called?result = f(x)f(x), as a statement
Which does add_time use?this one — it builds a new Timeincrement, in the next lesson

Both appear in this chapter, and the book recommends preferring the first whenever it is reasonable.

56. The procedure: prototype and patch

Pattern

Five steps, and the fifth is the one that decides whether you are finished.

  1. Write the basic calculation, ignoring the complications you can foresee.
  2. Test it on an input where a complication actually arises — not on round numbers.
  3. Read the wrong result and identify which case it exposes.
  4. Patch that case, keeping the fix as small as the failure warrants.
  5. Argue that no further cases exist, or accept that you only know the errors you have found.

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

57. Check yourself 1 of 3: pure functions

Check

Two conditions, and both are required.

Check your understanding

Which function is NOT pure?

  • A. print_time, which displays a Time (correct)
  • B. add_time, which returns a new Time
  • C. is_after, which returns a boolean
  • D. time_to_int, which returns a number

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.

Why B tempts people
It creates its own Time and returns it, modifying neither argument.
Why C tempts people
It reads both arguments and returns a comparison, with no effects at all.
Why D tempts people
It reads a Time and returns an integer, changing nothing.

58. Check yourself 2 of 3: the prototype's result

Check

No carrying.

Check your understanding

The prototype add_time is given 9:45:00 and 1:35:00. What does print_time show?

  • A. 10:80:00 (correct)
  • B. 11:20:00
  • C. An error about an invalid minute value
  • D. 10:20:00

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.

Why B tempts people
That is the correct answer, which the patched version produces after carrying.
Why C tempts people
Nothing checks the range. The Time class body is a docstring and declares no constraints.
Why D tempts people
This would require carrying the minutes without adding to the hour, which no version does.

59. Check yourself 3 of 3: the carry order

Check

Two conditionals, and their order matters.

Check your understanding

Why must the seconds carry be checked before the minutes carry?

  • A. Because the seconds carry can push the minutes to sixty, which the later check then catches (correct)
  • B. Because seconds are smaller than minutes
  • C. Because the minutes check would raise an error otherwise
  • D. It does not matter — both orders give the same result

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.

Why B tempts people
Size alone is not the reason; it is that one check's effect feeds the other.
Why C tempts people
Nothing raises in either order. The wrong order produces a wrong answer silently.
Why D tempts people
They differ on exactly the inputs where both carries fire, which is why that test case has to be constructed deliberately.

60. Where this shows up outside this course

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.

61. Confidence wager: commit before you check

Commit first

Answer, then rate your confidence.

Predict first

A function returns a correct value and prints a progress message. Is it pure?

  • No — a pure function has no effect other than returning a value
  • Yes — it returns a value and modifies no arguments
  • Yes, provided the message is only for debugging
  • Only if the printing happens after the return

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.

62. Explain it to someone else

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.

63. Exit ticket

Exit ticket

One honest answer. It decides what the next lesson opens with.

Predict first

Which of these is still least solid for you?

  • The Time class, and the exercises on it
  • What makes a function pure — both conditions
  • The prototype, the 10:80:00 result, and the patch
  • Prototype-and-patch as a plan, and how to test deliberately

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.

64. Synthesis: draw the map of this lesson

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.

65. What you can do now

Recap

Two pages, and a development plan with a name.

If you remember one thingIt is this
From the Time classA time is three numbers, and comparing it is comparing a tuple.
From pure functionsTwo conditions: no modification, and no effect besides returning.
From 10:80:00A semantic error: every step legal, and the meaning wrong.
From the patchCarry upward, because the seconds carry can create a minute overflow.
From the planPassing 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

Sources

  1. Think Python, 2nd edition — Allen B. Downey — Allen B. Downey, Think Python: How to Think Like a Computer Scientist, 2nd edition (Green Tea Press, 2015), §16.1-16.2, pp. 155-156
  2. Python documentation — Classes
  3. Python documentation — Built-in Functions

Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108