9b Looping with Indices, and Debugging by Testing

This lesson meets the puzzles a plain for loop cannot solve — comparing adjacent letters, and comparing a word with itself from both ends — gives three different solutions to the same problem, and closes with what testing can and cannot establish.

Subject: Python · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Lesson 9b Looping with Indices, and Debugging by Testing

Title

Python · Chapter 9 — Case study: word play

§9.4-9.5, pp. 86-87

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 §9.4-9.5, pp. 86-87 — the pages these objectives are drawn from

3. Before we start: what can a for loop not see?

Warm-up

The previous lesson's four functions all used a plain for loop. Find its limit.

Discussion prompt

You want to check whether a word's letters are in alphabetical order. Using for letter in word, what do you have access to on each pass — and what do you need that you do not have?

Hint: You need to compare two things.

Answer:

On each pass you have one letter and nothing else: not its position, and not the letter after it.

To check alphabetical order you need to compare each letter with the NEXT one, and the loop gives you them one at a time with no way to look ahead.

So this puzzle is genuinely different from the previous four. The book says so: for is_abecedarian we have to compare adjacent letters, which is a little tricky with a for loop — and it then gives three ways round it.

4. The one idea behind this lesson: comparing two positions at once

Concept

Every function in the previous lesson judged each letter on its own. These two judge each letter against another one — the next letter, or the letter the same distance from the other end — and that changes what the loop has to provide.

special case — A test case that is atypical or non-obvious, and therefore less likely to be handled correctly.

The book gives three solutions to the adjacent-letters problem, and comparing them is more useful than any one of them: a remembered previous value, recursion on a shorter word, and a while loop over indices.

Figure (svg): Two columns contrasting judging each letter alone with judging each letter against another

The right-hand column is what this lesson is about.

Think Python, 2nd edition — Allen B. Downey §9.4-9.5, pp. 86-86

5. Comparing adjacent letters with a remembered value

Section

Section 1

6. The first of three solutions

Concept

A for loop gives you one letter per pass. If you remember the previous one in a variable, you can compare the two — which turns a problem about pairs into a problem about one letter and one memory.

def is_abecedarian(word):
    previous = word[0]
    for c in word:
        if c < previous:
            return False
        previous = c
    return True
LineIts roleNote
previous = word[0]initialize before the loopthe first letter
if c < previouscompare with what came beforethe test
previous = cremember for the next passthe update
return Trueafter the loopno break in the order

The comparison uses the relational operator on strings from lesson 8c: c < previous asks whether c comes alphabetically before the letter that preceded it, which would be a break in the abecedarian trend.

Think Python, 2nd edition — Allen B. Downey §9.4-9.5, pp. 86-86

7. Picture it: one letter and one memory

Picture it

The loop still sees one letter at a time; the variable supplies the other half of each pair.

Figure (svg): The word abet drawn as boxes with arrows showing each letter compared against the remembered previous one

Three comparisons for four letters — each highlighted letter is compared with the one before it.

Notice the count: n letters give n-1 adjacent pairs, which is the fencepost arithmetic from lesson 8a and the source of every off-by-one in this lesson.

8. Worked example: why previous is initialized to word[0]

Worked example

The first pass compares the first letter with itself. Work out why that is harmless.

def is_abecedarian(word):
    previous = word[0]
    for c in word:
        if c < previous:
            return False
        previous = c
    return True
PassWhat happensEffect
pass 1c is word[0], previous is word[0]compares a letter with itself
is c < previous?a letter is not less than itselfFalse, so no early return
previous = cunchanged on this passthen the real comparisons begin

Notice the first pass is a wasted comparison.

Why: c and previous are the same letter, so the test asks whether a letter is less than itself — which is always False.

See why that is safe rather than merely harmless.

Why: A False test means no early return, so the wasted pass cannot produce a wrong answer. It costs one comparison.

Consider the alternative.

Why: Starting the loop at the second letter would avoid the waste but needs an index, which is what the third solution does.

Figure (svg): A ladder showing the value of previous after each pass of the loop over the word abet

The first comparison is redundant; the rest are the real work.

The first comparison is a letter against itself, which is always False, so it is wasted rather than wrong. Initializing previous to word[0] is what makes the first real pass work correctly.

Verify: Check what happens if previous is initialized to something else.

Why: Initializing to the empty string would make the first comparison c < '', which is False for every letter, so it also works by accident. Initializing to 'z' would make almost every word fail immediately. That the choice matters is worth knowing — the initialization is part of the algorithm, not boilerplate.

9. Predict: how many comparisons?

Prediction

Count the pairs, not the letters.

Predict first

For a six-letter word, how many ADJACENT PAIRS of letters are there?

  • Six
  • Five
  • Seven
  • Three

Correct: Five — n letters give n-1 adjacent pairs.

Why: This is the fencepost arithmetic from lesson 8a: six posts have five gaps between them. It is the source of every off-by-one in this lesson, and it is why the index version's loop condition is i < len(word) - 1 rather than i < len(word) — the last letter has no next letter to be compared with.

10. Worked example: a word that fails

Worked example

Find the pass where the trend breaks.

>>> is_abecedarian('flossy')
?
PairThe testWhat happens
f then l'l' < 'f'? nocontinue
l then o'o' < 'l'? nocontinue
o then s's' < 'o'? nocontinue
s then s's' < 's'? no — equal is allowedcontinue
s then y'y' < 's'? noreturn True

Check each adjacent pair.

Why: Five pairs for a six-letter word, and none of them breaks the order.

Notice the double letter.

Why: The pair s and s compares equal, and the test is strictly less-than, so it passes. Double letters are ok, which the exercise says explicitly.

Conclude.

Why: flossy is abecedarian: f, l, o, s, s, y are in non-decreasing alphabetical order.

Figure (svg): The state of the program after each line of Worked example a word that fails, drawn as a ladder with one rung per traced line

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

True. flossy passes, including its double s, because the test uses strict less-than and equal letters do not break the trend.

Verify: Check a word that should fail.

Why: is_abecedarian('banana') meets the pair b then a, and 'a' < 'b' is True, so it returns False on the second pass. Testing both a passing and a failing word is the minimum, and the double-letter case is a third worth adding because it is where a wrong comparison operator would show.

11. Trap: using less-than-or-equal in the comparison

Trap

The trap

A student writes if c <= previous: return False, reasoning that the letters should be strictly increasing.

Read alphabetical order as strictly increasing

Why: In a dictionary no two adjacent entries are identical, so strict increase seems right.

Now every word with a double letter fails. flossy, hilly and abbey are all rejected, and the exercise says explicitly that double letters are ok.

The fix

A break in the trend is a letter that comes strictly BEFORE its predecessor.

Test with strict less-than

Why: if c < previous, which is False when the letters are equal, so a double letter passes.

Test a word with a double letter deliberately

Why: flossy is the book's own example, and it is exactly the case that distinguishes the two operators.

This is a case where the specification carries a detail the obvious reading misses. Reading double letters are ok and then choosing an operator that rejects them is a specification bug rather than a coding one.

12. Predict: does flossy pass?

Prediction

Watch the double letter.

>>> is_abecedarian('flossy')
WordWhat happensResult
f, l, o, s, s, yeach pair checkedfive comparisons
the s, s pair's' < 's' is Falsenot a break
the resultno break foundTrue

Predict first

What does is_abecedarian('flossy') return?

  • True
  • False
  • None
  • An error

Correct: True — f, l, o, s, s, y are in non-decreasing order, and the double s does not break the trend because the test uses strict less-than.

Why: The exercise says double letters are ok, and the strict comparison is what implements that: 's' < 's' is False, so the equal pair does not trigger the early return. Using <= instead would reject flossy and every other word with a repeated letter — which is why flossy is the test case worth running.

13. Complete it: remember the previous letter

Faded example

Two blanks: the initialization and the update.

Fill in the blanks

def is_abecedarian(word):
previous = word[0]
for c in word:
if c < previous:
return False
previous = c
return True

Why: previous starts as the first letter, which makes the first comparison a letter against itself — wasted but harmless. The update at the end of the body is what makes the next pass compare against this letter. Omitting the update would compare every letter against the first one, which answers a different question entirely: whether every letter comes after the first.

14. Think it through: why does the for loop need help here?

Socratic

It was enough for four functions and is not enough for this one.

Discussion prompt

The previous lesson's functions all used a plain for loop over characters. Explain precisely what is different about is_abecedarian, in terms of what each pass needs.

Hint: Count how many letters each decision involves.

Answer:

Those functions decided about each letter in isolation: is this an e, is this in the forbidden set. One letter, one decision.

is_abecedarian decides about a PAIR, and the for loop supplies one letter per pass with no access to the next — so half of each comparison has to come from somewhere else.

The three solutions differ only in where that other half comes from: a remembered variable, a recursive call on the rest of the word, or an index that can reach both positions. Seeing them as three answers to one question is more useful than learning three functions.

15. The same problem, three ways

Section

Section 2

16. Recursion and an index loop

Concept

The book gives two more solutions to the same problem. Comparing them is the point: each supplies the missing half of the comparison differently, and each is idiomatic somewhere.

# recursion
def is_abecedarian(word):
    if len(word) <= 1:
        return True
    if word[0] > word[1]:
        return False
    return is_abecedarian(word[1:])
PartWhat it doesNote
the base casea word of 0 or 1 letterstrivially in order
the comparisonword[0] against word[1]both available at once
the recursive callon the rest of the worda shorter problem

The third version uses a while loop with an index, comparing the character at i with the character at i+1. The loop starts at 0 and ends when i is len(word)-1, so the last comparison is between the second-to-last character and the last.

Think Python, 2nd edition — Allen B. Downey §9.4-9.5, pp. 86-86

17. Picture it: where the other half comes from

Picture it

Three solutions, three sources for the second letter of each pair.

Figure (svg): Two columns comparing the three sources for the second half of each adjacent comparison

One problem, three ways to get at two positions at once.

None is best in general. The index version is the most direct; the recursion is the shortest to reason about; the remembered variable needs no index arithmetic at all.

18. Worked example: the recursive version

Worked example

Check it with the three questions from lesson 6b.

def is_abecedarian(word):
    if len(word) <= 1:
        return True
    if word[0] > word[1]:
        return False
    return is_abecedarian(word[1:])
CheckWhat it establishesNote
base case0 or 1 letterscorrect: nothing can be out of order
smaller inputword[1:] is one shortertermination guaranteed
the leapassume the rest is in orderthen check only the first pair

Check the base case.

Why: A word of one letter or none has no adjacent pairs, so it is trivially in order. The test uses <= 1 rather than == 1, so the empty string is covered too.

Check that the input shrinks.

Why: word[1:] is one character shorter than word, so repeated calls reach the base case.

Take the leap of faith.

Why: Assuming the rest of the word is in order, this word is in order provided its first two letters are — which is exactly what the second condition checks.

Figure (svg): A call diagram showing is_abecedarian on abet delegating to bet, then et, then t

Correct, by all three checks. The recursion compares one pair per level and delegates the rest, which is the leap of faith from lesson 6b in its most direct form.

Verify: Check the base case boundary against a one-letter word.

Why: is_abecedarian('a') returns True immediately, with no comparison at all — which is right, since one letter has no pair to be out of order with. That the base case is <= 1 rather than == 0 is what makes it work, and it is the kind of detail lesson 6c warned about.

19. Predict: what does the wrong bound do?

Prediction

The body reaches one position ahead.

i = 0
while i < len(word):
    if word[i+1] < word[i]:
        return False
    i = i+1
PassWhat happensResult
i = len-1the last valid indexword[i] is fine
word[i+1]index lenout of range
the resultIndexError on the last passloud

Predict first

What happens on the last pass of this loop?

  • It compares the last letter with itself
  • An IndexError, because word[i+1] is one past the end
  • It silently skips the last comparison
  • Nothing — the loop is correct

Correct: An IndexError — on the last pass i is len-1, so word[i+1] is word[len], which is out of range.

Why: The bound has to account for how far ahead the body reaches, not only for the valid indices of i itself. This failure is at least loud: it raises immediately rather than producing a wrong answer. The quieter version of the same mistake is stopping at len-2, which would silently skip the last pair.

20. Worked example: the index version and its bound

Worked example

The condition is not the usual one. The book explains why with a specific word.

def is_abecedarian(word):
    i = 0
    while i < len(word)-1:
        if word[i+1] < word[i]:
            return False
        i = i+1
    return True
PartValueNote
the conditioni < len(word)-1not the usual len(word)
for 'flossy'len is 6, so i runs 0 to 4five passes
the last passi = 4 compares word[4] with word[5]the last pair

Notice the unusual condition.

Why: It stops one earlier than a normal traversal, because the body uses word[i+1] and the last letter has no successor.

Follow the book's own check.

Why: The length of flossy is 6, so the last time the loop runs is when i is 4, which is the index of the second-to-last character.

Confirm the last comparison is right.

Why: On the last iteration it compares the second-to-last character to the last, which is what we want.

Figure (svg): The word flossy with the index i shown running from zero to four and each comparison spanning two adjacent letters

Five values of i, five comparisons, six letters. The last letter is reached only as word[i+1].

The condition is i < len(word)-1 because the body reaches one position ahead. The loop makes five passes for a six-letter word, which is the number of adjacent pairs.

Verify: Check what the usual condition would do.

Why: With i < len(word), the last pass would have i = 5 and word[i+1] would be word[6], which is out of range — an IndexError. The unusual bound is not a stylistic choice; it is forced by the body reaching ahead, and it is the standard shape whenever a loop looks at i and i+1 together.

21. Trap: using the normal loop bound with a lookahead

Trap

The trap

A student writes while i < len(word) with a body that compares word[i] and word[i+1].

Use the traversal condition learned in lesson 8a

Why: It is the correct bound for a loop that touches only word[i], and it has been right every time so far.

On the last pass i is len-1 and word[i+1] is out of range, raising an IndexError — which at least is loud.

The fix

The bound depends on the furthest position the body reaches.

If the body uses i+1, stop at len - 1

Why: So that the last pass has a valid i+1.

Count the passes and check against the number of pairs

Why: n letters have n-1 pairs, so the loop should run n-1 times.

Stating it as a rule about the body rather than about traversals makes it transfer: a loop reaching i+2 would stop at len - 2, and the same reasoning gives the bound each time.

22. Compare: the three solutions

Comparison

Fill the blanks. Each supplies the second letter of the pair differently.

Comparison matrix

VersionWhere the other letter comes fromWhat it needs
remembered variablea variable holding the previous letteran initialization and an update
recursionword[0] and word[1], then a recursive calla base case for short words
index loopword[i] and word[i+1] togethera loop bound of len - 1

Each has one thing that can go wrong, and they are different things — which is a reason to know all three rather than only the one you like.

23. Rank: the recursive version's checks

Ranking

Lesson 6b's three checks, applied in order.

Put in order

  1. is the base case correct for a word of one letter or none?
  2. is word[1:] strictly shorter than word?
  3. assuming the rest is in order, is this word in order if its first two letters are?
  4. does every path reach a return statement?

Why: The base case is checked first, because it involves no assumption. Then termination, which the leap of faith depends on. Then the leap itself. Then the every-path check from lesson 6a, which is about the function's shape rather than about the recursion. All four are needed, and the leap alone would pass a function with no reachable base case.

24. Eliminate: which version would you choose here?

Elimination

All three are correct. One is the best fit for this particular problem.

Eliminate the wrong options

For is_abecedarian specifically, which version is the clearest?

  • A. the index loop, comparing word[i] with word[i+1]
  • B. the remembered-variable version
  • C. the recursive version
  • D. a plain for loop over characters

Survives elimination: A

Why: The index version says exactly what the problem is: compare each letter with the next. Its one hazard is the loop bound, which is visible on the line where it appears. The other two are correct and each carries something the reader has to work out — a redundant first comparison, or a slice that copies the rest of the word. That said, this is a judgement rather than a fact, and the recursive version is the clearest of the three for a reader who is comfortable with recursion.

25. Two indices moving toward each other

Section

Section 3

26. is_palindrome

Concept

A palindrome reads the same forwards and backwards, so testing for one means comparing the first character with the last, the second with the second-to-last, and so on — which needs two indices moving in opposite directions.

def is_palindrome(word):
    i = 0
    j = len(word)-1
    while i < j:
        if word[i] != word[j]:
            return False
        i = i+1
        j = j-1
    return True
PartIts roleNote
istarts at 0, moves upthe front
jstarts at len-1, moves downthe back
while i < jstop when they meet or crossthe bound
the comparisonword[i] against word[j]matching positions

Compare this with the is_reverse function from lesson 8c, which had exactly this shape and two bugs in it. Here j starts at len(word)-1, which was the first of those bugs, and the condition is i < j, which addresses the second.

Think Python, 2nd edition — Allen B. Downey §9.4-9.5, pp. 86-87

27. Picture it: the two indices meeting in the middle

Picture it

Each pass compares one pair and moves both indices inward.

Figure (svg): The word noon with arrows showing i moving right from the first character and j moving left from the last

First pass compares indices 0 and 3; second compares 1 and 2; then i and j meet.

The loop stops when i and j meet or cross, which for a four-letter word is after two passes — half the length, which is exactly right since each pass handles two positions.

28. Worked example: why the condition is i < j

Worked example

Two indices need a different kind of bound. Work out what it is.

# 'noon', length 4
#   pass 1: i=0, j=3   compare 'n' and 'n'
#   pass 2: i=1, j=2   compare 'o' and 'o'
#   now i=2, j=1       i < j is False, stop

# 'racecar', length 7
#   ... pass 3: i=2, j=4
#   now i=3, j=3       i < j is False, stop
CaseHow the loop endsNote
even lengththe indices cross2 and 1
odd lengththe indices meet3 and 3
i < jhandles bothstops in either case

Trace an even-length word.

Why: The indices cross without ever being equal: i becomes 2 while j becomes 1, and i < j is False.

Trace an odd-length word.

Why: They meet exactly, both landing on the middle character. i < j is False there too.

Notice why the middle character needs no comparison.

Why: In an odd-length palindrome the middle character is compared with itself, which is always equal — so skipping it is correct rather than a shortcut.

Figure (svg): A flow chart showing two indices being compared and then moved toward each other until they meet

i < j handles both cases: it becomes False when the indices meet or cross. The loop runs half as many times as the word is long, because each pass settles two positions.

Verify: Check a one-letter word and the empty string.

Why: For 'a', i is 0 and j is 0, so i < j is False immediately and the function returns True — correct, since a single letter is a palindrome. For the empty string, j is -1 and the condition is also False, giving True. Both special cases work without any extra code, which is worth confirming rather than assuming.

29. Predict: how many passes?

Prediction

Each pass handles two positions.

Predict first

For the six-letter word 'redder', how many times does the is_palindrome loop run?

  • Six
  • Five
  • Three
  • Two

Correct: Three — each pass compares one pair, and six letters make three pairs.

Why: The indices start at 0 and 5, then 1 and 4, then 2 and 3, and after that i is 3 and j is 2, so i < j is False. Three passes for six letters is half the length, which is the general rule: each pass settles two positions, so the loop runs len divided by two times, rounded down.

30. Worked example: reduction, again

Worked example

The book offers a one-line alternative. Recognise the move.

def is_palindrome(word):
    return is_reverse(word, word)
FunctionWhat it asksNote
is_reverse(a, b)is a the reverse of b?from lesson 8c
is_reverse(word, word)is the word the reverse of itself?which is a palindrome
the reductionno new logicone line

Recall what is_reverse does.

Why: It returns True when one word is the reverse of the other — the function with two bugs from lesson 8c.

State the palindrome definition in those terms.

Why: A palindrome is a word that is its own reverse.

Write the reduction.

Why: is_palindrome(word) is is_reverse(word, word). Or we could reduce to a previously solved problem, as the book puts it.

Figure (svg): The state of the program after each line of Worked example reduction, again, drawn as a ladder with one rung per traced line

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

One line, using a function already written. This is the second reduction in two lessons, and it is worth noticing that both were found by restating the definition rather than by studying the code.

Verify: Check that the reduction depends on is_reverse being correct.

Why: is_reverse had two bugs in lesson 8c, one of which was left as an exercise. Reducing to a function that is not yet correct inherits its bugs — which is the maintenance argument for reduction working in reverse. A fix to is_reverse fixes both, and so does a bug.

31. Trap: a condition of i != j

Trap

The trap

A student writes while i != j, reasoning that the loop should stop when the indices meet.

Think only about the odd-length case

Why: There the indices really do meet exactly, so the condition works.

For an even-length word they cross without ever being equal — 2 and 1 — so the condition never becomes false and the indices run off both ends of the string.

The fix

Use i < j, which becomes false whether the indices meet or cross.

Test both an even-length and an odd-length word

Why: They exercise genuinely different paths through the loop's ending, and only one of them exposes this bug.

Prefer inequalities to equalities in loop conditions

Why: Which is lesson 6c's advice about base cases, in loop form: an exact test can be stepped over.

The parity of the length is a special case in the sense §9.5 means: non-obvious, and where errors lurk. A test suite of only odd-length palindromes would pass this bug entirely.

32. Watch the indices: is_palindrome on 'noon'

Invariant

Step through and watch the two indices approach each other.

Step through it

Why did the loop stop after two passes for a four-letter word, and what would happen with a condition of i != j?

  1. First pass: the outermost pair. Both are 'n', so the loop continues.
  2. Second pass: the indices have moved inward by one each. Both are 'o'.
  3. The indices have now crossed — i is 2 and j is 1 — so i < j is False and the function returns True.

Two passes for four letters, because each pass settles two positions. With i != j the indices would cross without ever being equal — 2 and 1 — and the loop would run past both ends of the string.

33. Discriminate: one index or two?

Discrimination

Ask which positions each comparison involves.

Sort into buckets

For each task, does it need one index with a lookahead, or two indices moving toward each other?

one index with a lookahead
are the letters in alphabetical order?; does any letter repeat immediately?; is every letter followed by a different letter?
two indices from both ends
is the word a palindrome?; is the first half the same as the reversed second half?; does the word read the same backwards?
one
Each compares a position with the one immediately after it, so a single index and a lookahead of one suffices — with the loop stopping at len minus one.
two
Each compares a position with its mirror image at the other end, which no single index can reach. Two indices moving inward is the standard shape.

34. Explain it: why i < j rather than i != j?

Explain it

The bug only shows for one parity of length.

Discussion prompt

A classmate's palindrome checker works for 'racecar' and crashes on 'noon'. Their condition is i != j. Explain what is happening and what test would have caught it.

Hint: The two words differ in one structural way.

Answer:

Say: racecar has an odd length, so the indices meet exactly at the middle and the condition becomes false. noon has an even length, so they cross without ever being equal — 2 and 1 — and the loop never stops.

The fix is i < j, which is false whether they meet or cross.

The test that would have caught it: any even-length word. This is what §9.5 means by a special case — the parity of the length is not an obvious dimension to test along, and it is exactly where the error lurks.

35. Choosing test cases

Section

Section 4

36. Obvious cases, subcases, and special cases

Concept

The functions in this chapter are relatively easy to test because you can check the results by hand. Even so, it is somewhere between difficult and impossible to choose a set of words that test for all possible errors.

The empty string is the book's own example of a special case, and it is the one most likely to be missed — precisely because it is not a word anybody would think to try.

Think Python, 2nd edition — Allen B. Downey §9.4-9.5, pp. 87-87

37. Picture it: the dimensions to test along

Picture it

Each dimension is a way a function can be wrong that the others would not catch.

Figure (svg): Two columns listing test dimensions against the bug each one is likely to catch

Four cases, four distinct bugs, none of which the others would find.

Notice that each row names a specific bug. A test case chosen without a bug in mind is much less likely to find one.

38. Worked example: a test set for has_no_e

Worked example

The book's own analysis, made concrete.

# should return False (contains an e):
#   'end'      e at the beginning
#   'the'      e at the end
#   'bed'      e in the middle
# should return True:
#   'jazz'     no e at all
#   'a'        a very short word
#   ''         the empty string
Test caseWhat it catchesNote
e at the startcatches a traversal starting at index 1silent bug
e at the endcatches a traversal stopping one shortsilent bug
the empty stringcatches code assuming word[0] existsloud bug

Cover both obvious cases.

Why: Words that have an e should return False, and words that do not should return True. You should have no trouble coming up with one of each.

Cover the subcases within each.

Why: Among the words that have an e, test words with an e at the beginning, the end, and somewhere in the middle.

Add the special case.

Why: Test long words, short words, and very short words, like the empty string.

Figure (svg): The state of the program after each line of Worked example a test set for has no e, drawn as a ladder with one rung per traced line

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

Six test cases along three dimensions. Each one is chosen with a specific failure in mind, which is what makes it worth running rather than merely reassuring.

Verify: Ask which of the six a naive test set would omit.

Why: Almost anybody tests one word with an e and one without. The position subcases and the empty string are the ones that get skipped — and they are precisely the ones that catch the two silent traversal bugs from lesson 8a and the boundary bug from this lesson.

39. Sort: which dimension does each test case exercise?

Sorting

Each case is chosen to catch something specific.

Sort into buckets

For testing has_no_e, what does each case exercise?

the position of the target
'end'; 'the'; 'bed'
the length of the word
''; 'a'
the obvious case: no target at all
'jazz'
pos
Beginning, end and middle. These catch traversals that start at the wrong index or stop one short — the two silent failures from lesson 8a.
len
The empty string and a single character. These catch code that assumes at least one character exists, such as an initialization using word[0].
obv
A word with no e at all, which is the other half of the obvious pair and confirms the True path works.

40. Worked example: testing against the word list

Worked example

A hundred thousand words is a test too, and it has a specific blind spot.

# scan the output of has_no_e over words.txt
#
# you might catch:  words that should NOT be included, but are
# you will not catch: words that SHOULD be included, but aren't
Error kindCan scanning find it?Note
false positivesa word with an e in the outputvisible by scanning
false negativesa word without an e missinginvisible: you cannot scan absences
the asymmetryone kind is findable, the other is nota real limitation

Notice what scanning can find.

Why: In addition to your own test cases you can test with a word list, and by scanning the output you might be able to catch errors.

Notice what it cannot.

Why: But be careful: you might catch one kind of error — words that should not be included, but are — and not another — words that should be included, but are not.

Draw the conclusion.

Why: A large data set is a supplement to hand-chosen cases, not a replacement. It finds a different kind of error and is blind to its complement.

Figure (svg): Two columns comparing hand-chosen test cases with scanning a large output, showing what each can and cannot find

Neither replaces the other. The right column is blind to omissions.

Scanning output finds words that should not be there and cannot find words that are missing, because absences are invisible. Both kinds of error exist, so both kinds of test are needed.

Verify: Consider how you would look for a missing word.

Why: You would have to pick a word you know should appear and check that it does — which is a hand-chosen test case again. The large data set never removes the need for cases you designed, which is the practical form of Dijkstra's point.

41. Trap: testing only the cases you thought of while writing

Trap

The trap

A student writes a function, tests it on the examples they had in mind while writing it, and it passes.

Test with the cases that shaped the code

Why: They are the ones to hand, and they exercise the logic you were thinking about.

Those are exactly the cases the code was built to handle. The errors lurk in the cases you did not think of, which is why they are still there.

The fix

Choose test cases along dimensions rather than by recall.

List the dimensions first: position, length, parity, emptiness

Why: Then pick one case per dimension, whether or not it occurred to you while writing.

For each case, name the bug it would catch

Why: A test case with no bug in mind is much less likely to find one.

The book's phrase for the ones that get missed is special case: one of the non-obvious cases where errors often lurk. Non-obvious is the operative word — obvious cases are already handled, because they were in mind while the code was written.

42. Predict: which version fails on the empty string?

Prediction

One of the three is_abecedarian versions has a problem here.

# version 1: previous = word[0]
# version 2: if len(word) <= 1: return True
# version 3: while i < len(word)-1
VersionWhat happens on ''Result
version 1word[0] on an empty stringIndexError
version 2len is 0, so <= 1 is Truereturns True
version 3len-1 is -1, so 0 < -1 is Falsereturns True

Predict first

Which version raises an error on the empty string?

  • The remembered-variable version, because it starts with word[0]
  • The recursive version
  • The index version
  • All three

Correct: The remembered-variable version — it initializes previous to word[0], which raises an IndexError for an empty string.

Why: The other two handle it without any special code: the recursive base case tests len <= 1 and the index loop's condition is immediately false when len-1 is -1. This is why the empty string is the book's example of a special case: three correct-looking versions of one function, and only one of them fails on it.

43. Complete it: guard the empty string

Faded example

One guardian, from lesson 6c.

Fill in the blanks

def is_abecedarian(word):
if len(word) == 0:
return True
previous = word[0]
for c in word:
if c < previous:
return False
previous = c
return True

Why: The guardian returns before word[0] is evaluated, which is what makes the initialization safe. An empty string is trivially in alphabetical order, so True is the right answer rather than an evasion. Note the ordering requirement from lesson 6c: the guard must come before the line it protects, or it protects nothing.

44. Explain it yourself: what makes a case *special*?

Explain it to yourself

The book's definition is short. Make it operational.

Discussion prompt

The book defines a special case as one that is atypical or non-obvious, and therefore less likely to be handled correctly. Explain why non-obviousness is the operative property, rather than rarity.

Hint: Think about when the code was written.

Answer:

Because a case you thought of while writing the code is already handled — the code was shaped around it. Rarity is irrelevant if it was in mind.

A non-obvious case is one that did not occur to you, which is precisely why the code may not handle it. The empty string is the standard example: nobody writing has_no_e is picturing an empty word.

So the practical technique is to generate cases mechanically along dimensions — position, length, parity, emptiness — rather than by asking yourself what to test. Asking yourself returns the cases you already thought of.

45. What testing can and cannot establish

Section

Section 5

46. Dijkstra's remark

Concept

In general, testing can help you find bugs, but it is not easy to generate a good set of test cases — and even if you do, you cannot be sure your program is correct.

# Program testing can be used to show the presence of bugs,
# but never to show their absence!
#
#   -- Edsger W. Dijkstra
OutcomeWhat it establishesNote
a failing testproves a bug existsdefinitive
a passing testproves nothing about other inputsnot definitive
all tests passingno bug found YETnot the same as no bug

This is the same asymmetry as the search pattern in the previous lesson, applied to programs rather than to letters: one counterexample disproves a universal claim, and no number of confirming examples proves one.

Think Python, 2nd edition — Allen B. Downey §9.4-9.5, pp. 87-87

47. Picture it: the asymmetry, one more time

Picture it

It is the same shape as the search pattern, and that is not a coincidence.

Figure (svg): Two columns contrasting what a failing test proves with what a passing test proves

Presence can be shown; absence cannot.

The same asymmetry decided where the returns go in a search, why avoids needs the whole loop, and now what a green test suite means. It is one of the most transferable ideas in the book.

48. Worked example: a passing test that proves nothing

Worked example

The is_reverse function from lesson 8c is the case in point.

# after fixing the first bug:
>>> is_reverse('pots', 'stop')
True
# correct answer, and the loop ran three times for four letters
ObservationWhat it isNote
the answerTruecorrect
the loop countthree for four lettersone comparison skipped
what the test provedthis input worksnothing about others

Note the test passed.

Why: The function returned the correct answer for pots and stop.

Note the bug was still there.

Why: The loop ran three times for four-character words, so one pair was never compared.

Draw the conclusion.

Why: The test passed and the function was wrong. A passing test shows that this input works, and nothing more.

Figure (svg): The state of the program after each line of Worked example a passing test that proves nothing, drawn as a ladder with one rung per traced line

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

The test proved that one input worked. The second bug survived it entirely, and was found by counting passes rather than by testing — which is exactly Dijkstra's point.

Verify: Ask what test WOULD have caught it.

Why: One where the skipped pair differs: two words that match everywhere except the position the loop never compares. Constructing such a case requires knowing where the gap is, which means the bug had to be found first. That circularity is why testing is a supplement to reasoning rather than a substitute for it.

49. Predict: what does a passing test establish?

Prediction

Be precise about the scope of the claim.

Predict first

A function passes all ten of its tests. What has been established?

  • The function is correct
  • The function is correct for those ten inputs, and nothing about any other input
  • The function has no bugs in the tested lines
  • Nothing at all

Correct: That the function is correct for those ten inputs, and nothing about any other input.

Why: Testing can be used to show the presence of bugs, but never to show their absence. Ten passing tests are ten pieces of evidence about ten inputs, and a program with an infinite input space is not covered by any finite number of them. The fourth option overstates the limitation in the other direction — the tests do establish something real, just much less than correct.

50. Worked example: what to do given the limitation

Worked example

Testing is limited and it is still worth doing. Both halves matter.

# testing is limited, so:
#   1. choose cases along dimensions, not by recall
#   2. check the process, not only the answer
#   3. reason about the code as well as running it
#   4. treat a passing suite as 'no bug found yet'
TechniqueWhat it addsNote
dimensionsposition, length, parity, emptinessmechanical generation
the processcount the passes, check the boundscatches silent bugs
reasoningthe leap of faith, termination proofscovers all inputs at once

Generate cases mechanically.

Why: Because the cases you think of are the ones already handled.

Check the process as well as the output.

Why: Counting loop passes found the second is_reverse bug when no test did.

Reason as well as test.

Why: The three checks for a recursive function from lesson 6b cover every input at once, which no finite test set can.

Figure (svg): A diagram showing testing and reasoning as two complementary ways to gain confidence in a program

Testing cannot prove absence; reasoning can be wrong. Together they are what you have.

Four techniques, and the last is an attitude: a passing suite means no bug has been found yet. Testing shows the presence of bugs, never their absence.

Verify: Compare with the recursion checks from lesson 6b.

Why: Those checks — base case correct, input shrinks, this level right given the leap — establish correctness for every input, not just tested ones. That is what reasoning buys over testing, and it is why the book teaches both.

51. Trap: treating a green test suite as proof

Trap

The trap

Every test passes, so the function is declared correct and the work moves on.

Read all tests pass as no bugs

Why: It is the strongest evidence available and it feels conclusive.

It means no bug was found by these particular inputs. The is_reverse function passed its test with a comparison silently missing, and every additional passing test would have said the same.

The fix

Read a passing suite as no bug found yet.

Ask what the tests do NOT cover

Why: Which dimensions were not varied? Which paths through the code were never taken?

Supplement with reasoning about all inputs

Why: A termination proof or the leap of faith covers cases no test enumerated.

Dijkstra's remark is not a counsel of despair — testing genuinely finds bugs and is worth doing. It is a statement about what the result means, and getting that right changes how much confidence a green suite should buy.

52. Two truths and a lie: testing

Two truths and a lie

Two are true. Keep the lie.

Eliminate the wrong options

Rule out the two true statements.

  • A. A failing test proves a bug exists
  • B. The empty string is a good special case to test
  • C. A comprehensive test suite proves a program has no bugs

Survives elimination: C

Why: C is the lie, and it is the most consequential misconception in software. Program testing can be used to show the presence of bugs, but never to show their absence. However comprehensive a finite test suite is, the untested inputs remain untested — and the is_reverse function passed its test with a comparison missing entirely.

53. Analogical pivot: where you have met this asymmetry before

Analogy

It is the same shape four times in this book.

Match the pairs

  • a. the search pattern's two returns
  • b. avoids returning True after the loop
  • c. a failing test versus a passing suite
  • d. proving a loop terminates versus observing that it has
  • r1. one match settles it; absence needs the whole loop
  • r2. one bad letter disqualifies; qualification needs every letter
  • r3. one failure proves a bug; passing proves nothing general
  • r4. one non-terminating input disproves; many terminating ones do not prove

Why: All four are the same logical asymmetry: a universal claim is disproved by one counterexample and proved only by exhaustion or by argument. Recognising it as one idea rather than four coincidences is worth more than any of them individually — and it is why the Collatz sequence in lesson 7a remains unproved despite billions of confirming examples.

54. Where the asymmetry matters most

Real world

It is not a programming idea.

Discussion prompt

Give an example from outside programming where one counterexample settles a question and no number of confirming cases does. What do people do when they need confidence anyway?

Hint: Science is the standard case.

Answer:

A scientific hypothesis is the standard example: one reproducible contrary result refutes it, and no number of confirmations proves it. Popper made this the centre of his account of science.

What people do is exactly what programmers do: they test hard in the places most likely to fail, they reason about mechanisms rather than only collecting instances, and they hold their conclusions provisionally.

The practical translation for a program is the same. Test the special cases where errors lurk, reason about the loop bounds and the base cases, and treat a green suite as the absence of found bugs rather than the presence of correctness.

55. Compare: the three solutions to one problem

Comparison

Fill the blanks. Each supplies the second half of the comparison differently, and each has its own hazard.

Comparison matrix

VersionHow it sees two lettersWhat can go wrong
remembered variablekeeps the previous letter in a variableword[0] on an empty string
recursioncompares word[0] with word[1], then recurses on word[1:]a base case that misses the empty string
index loopreaches word[i] and word[i+1] at oncea loop bound that does not allow for the lookahead

Three correct solutions with three different failure modes — which is a good argument for knowing all three rather than only the one you find natural.

56. The procedure: writing a loop that compares two positions

Pattern

Six steps, and the fourth is where the off-by-one lives.

  1. Work out which two positions each comparison involves: adjacent, or mirrored from the ends.
  2. For adjacent positions, use one index and reach ahead — or remember the previous value in a variable.
  3. For mirrored positions, use two indices starting at 0 and len minus one and moving toward each other.
  4. Set the bound from the furthest position the body reaches: len minus one for a lookahead of one, or i < j for two indices.
  5. Count the comparisons and check: n items give n-1 adjacent pairs, or n divided by two mirrored pairs.
  6. Test both parities of length, both ends, and the empty string.

Step 5 catches the bugs that step 6 might miss. The is_reverse function passed its test while making one comparison too few, and only the count revealed it.

Python documentation — More Control Flow Tools More Control Flow Tools

57. Check yourself 1 of 3: the lookahead bound

Check

The body reaches one position past i.

i = 0
while i < len(word)-1:
    if word[i+1] < word[i]:
        return False
    i = i+1
PartValueNote
the bodyuses word[i+1]reaches one ahead
the boundlen - 1so i+1 is always valid
passesn - 1 for n lettersthe number of pairs

Check your understanding

Why is the condition i < len(word)-1 rather than i < len(word)?

  • A. Because the last letter never needs checking
  • B. Because the body uses word[i+1], which would be out of range on the last pass (correct)
  • C. Because loops always stop one before the end
  • D. Because len(word) is not a valid index

Answer: B

Why: The bound depends on the furthest position the body reaches. With word[i+1] in the body, the last valid value of i is len-2, so the condition must stop at len-1. A loop touching only word[i] would use the ordinary bound — the rule is about the body, not about traversals in general.

Why A tempts people
The last letter is checked — as word[i+1] on the final pass. It simply never plays the role of word[i].
Why C tempts people
Ordinary traversals stop at len, as in lesson 8a. This one is different because of the lookahead.
Why D tempts people
True but not the reason. The problem is that i+1 reaches len when i is len-1, which is one step earlier than the ordinary bound would allow for.

58. Check yourself 2 of 3: two indices

Check

One condition handles both parities of length.

i = 0
j = len(word)-1
while i < j:
    ...
    i = i+1
    j = j-1
CaseHow it endsNote
even lengththe indices cross: 2 and 1i < j is False
odd lengththe indices meet: 3 and 3i < j is False
i != jwould miss the crossing caseinfinite loop

Check your understanding

Why is the condition i < j rather than i != j?

  • A. They are equivalent
  • B. Because for an even-length word the indices cross without ever being equal (correct)
  • C. Because i is always smaller than j
  • D. Because != cannot be used on integers

Answer: B

Why: For a four-letter word the indices go 0 and 3, then 1 and 2, then 2 and 1 — crossing without ever being equal. With i != j the loop would never stop and both indices would run off the ends of the string. The inequality is false whether they meet or cross, which covers both parities.

Why A tempts people
They are equivalent only for odd-length words, where the indices meet exactly. The even case is precisely where they differ.
Why C tempts people
It is at the start and stops being so once they cross, which is exactly the situation the condition has to detect.
Why D tempts people
!= works fine on integers. The problem is what it fails to detect, not whether it is legal.

59. Check yourself 3 of 3: what testing establishes

Check

Be precise about the direction of the claim.

Check your understanding

According to Dijkstra, what can program testing establish?

  • A. The absence of bugs, if the tests are comprehensive enough
  • B. The presence of bugs, but never their absence (correct)
  • C. Neither presence nor absence
  • D. Both, provided every branch is exercised

Answer: B

Why: Program testing can be used to show the presence of bugs, but never to show their absence. A failing test is a counterexample and settles the matter; a passing test is one confirming instance and leaves every untested input open. This is the same asymmetry as the search pattern, applied to programs.

Why A tempts people
No finite test suite covers an infinite input space. Comprehensiveness raises confidence and does not change the logic.
Why C tempts people
Testing genuinely does establish presence — a failing test is conclusive evidence of a bug, which is why testing is worth doing at all.
Why D tempts people
Exercising every branch is a useful goal and still does not cover every input. A branch can be correct for the tested value and wrong for another.

60. Where this shows up outside this course

Real world

Comparing adjacent items is one of the most common operations there is.

Discussion prompt

Think of something you check by comparing consecutive items rather than looking at each alone — a sorted list, a sequence of readings, a series of dates. How many comparisons does it take for n items, and where do the errors happen?

Hint: The count is the fencepost answer.

Answer:

Checking a list is sorted, checking that dates are consecutive, checking that a sequence of readings never drops — all compare each item with its neighbour.

For n items there are n-1 comparisons, which is the fencepost count from lesson 8a: ten panels need eleven posts, and ten posts have nine gaps.

The errors happen at the ends. Either you compare one pair too many and run off the end, or one too few and miss the last pair — which is exactly the pair of bugs this lesson's loop bound is designed to avoid, and exactly where testing must be aimed.

61. Confidence wager: commit before you check

Commit first

Answer, then rate your confidence. It combines the loop bound with the special case.

Predict first

The remembered-variable version of is_abecedarian starts with previous = word[0]. What happens when it is called with the empty string?

  • It returns True, correctly
  • It returns False
  • An IndexError, because word[0] does not exist
  • It loops forever

Correct: An IndexError — the empty string has no character at index 0, and the initialization runs before the loop that would have handled the empty case.

Why: The other two versions handle the empty string without any special code: the recursive base case tests len <= 1, and the index loop's condition is 0 < -1, which is immediately false. Only the remembered-variable version reaches for a character before checking that one exists. This is exactly what the book means by a special case — atypical, non-obvious, and where errors lurk — and it is worth noticing that three correct-looking versions of the same function differ precisely on the input nobody thinks to try.

62. Explain it to someone else

Explain it

The limits of testing are worth stating carefully, because overstating them is as bad as ignoring them.

Discussion prompt

A classmate says that since testing cannot prove correctness, there is no point writing tests. Give them the accurate version of the claim and say what testing is genuinely for.

Hint: The asymmetry cuts one way, not both.

Answer:

Say: testing shows the presence of bugs, never their absence. A failing test is conclusive — it proves a bug exists — and that is enormously valuable.

What it cannot do is prove there are none, so a green suite means no bug found yet rather than correct. That is a statement about what the result means, not an argument against running them.

Then add what fills the gap: reasoning that covers every input at once — the three recursion checks, a termination argument, checking loop bounds against the number of pairs. Testing and reasoning find different things, and the is_reverse bug that survived its test was found by counting passes rather than by either testing harder or reasoning less.

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?

  • Why a plain for loop cannot compare adjacent letters
  • The loop bound when the body reaches one position ahead
  • Two indices moving toward each other, and why the condition is i < j
  • Choosing test cases, and what a passing test establishes

Correct: Whichever you picked is the right answer — this one is for you, not for a mark.

Why: The first is a single observation that unlocks the rest of the lesson. The lookahead bound is the most common off-by-one you will meet from here on, and stating it as a rule about the body rather than about traversals is what makes it transfer. The two-index shape recurs constantly — palindromes, merging, comparing sequences — and its condition is subtle enough to be worth deriving once. And the testing section is the most portable thing in the chapter: Dijkstra's asymmetry is the same one that shaped every search function you have written.

64. Synthesis: draw the map of this lesson

Connect it up

One page, from memory.

Draw it

Draw a six-letter word twice. On the first copy, mark the five adjacent pairs and write the loop bound that produces exactly those five comparisons. On the second, mark the three mirrored pairs and the path of the two indices, and write the condition that stops them correctly for both odd and even lengths. Then, below, list four test cases for one of these functions and name, for each, the specific bug it would catch.

65. What you can do now

Recap

Two pages, and chapter 9 is finished: the puzzles a plain for loop cannot solve.

If you remember one thingIt is this
From the three versionsOne problem, three ways, three different failure modes.
From the lookaheadThe bound depends on how far ahead the body reaches, not on the traversal.
From two indicesi < j stops whether they meet or cross. i != j misses the even case.
From special casesNon-obvious is the point. The cases you thought of are already handled.
From DijkstraTesting shows the presence of bugs, never their absence.

Chapter 10 introduces the type that changes most of what you know: a list is a sequence like a string, and unlike a string it can be modified — which brings a new kind of power and an entirely new category of bug.

Think Python, 2nd edition — Allen B. Downey §9.4-9.5, pp. 86-87 — 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), §9.4-9.5, pp. 86-87
  2. Python documentation — More Control Flow Tools
  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