Course outline · 0% complete

0/29 lessons0%

Course overview →

Bug Hunt: Four Broken Programs

lesson 8-3 · ~12 min · 25/29

The rules of the hunt

Four small programs, each crashing with a different bestiary entry. For each one, run the full lesson 7-1 loop:

  1. run it and read the traceback bottom-up (lesson 8-1)
  2. let the exception type narrow the hypothesis (lesson 8-2)
  3. change one thing and rerun

The expected output under each block tells you what the healthy program prints. No peeking at hints until the trace has told you its story.

Hunt 1, a NameError

The program crashes with a NameError, and the message quotes the name Python could not find. Before the fix, the call site read celsius_to_farenheit(t).

def celsius_to_fahrenheit(celsius):
    return celsius * 9 / 5 + 32

for t in [0, 100, 37]:
    print(t, "->", celsius_to_fahrenheit(t))

Output

0 -> 32.0
100 -> 212.0
37 -> 98.6

The trace ends with NameError: name 'celsius_to_farenheit' is not defined, and comparing that spelling with the def line letter by letter is the whole diagnosis. The definition says fahrenheit with an h after the a, and the call site dropped it.

The fix goes on the call site because the definition matches the real word. Renaming the function to the misspelling would also make the program run, and it would leave a permanent misspelling in the codebase for every future caller to reproduce.

Note what the exception type ruled out immediately. NameError means no such name exists at all, so nothing about the conversion arithmetic was ever in question, and the 9 / 5 + 32 line never even ran.

The outputs are floats rather than ints because / always produces a float in Python 3, so 0 degrees Celsius prints as 32.0. That is correct here, and it is the sort of detail an assertion has to match exactly if you write a test for this.

Hunt 2, an IndexError

The program crashes with an IndexError inside the comprehension. Before the fix, it read w[len(w)].

words = ["testing", "is", "detective", "work"]
lasts = [w[-1] for w in words]
print(lasts)

Output

['g', 's', 'e', 'k']

A string of length 7 has indexes 0 through 6, so w[len(w)] is always one past the end. That is a textbook off-by-one, which is lesson 3-2's whole topic arriving as a crash rather than as a wrong answer.

w[len(w) - 1] works and Python has a cleaner spelling, since w[-1] counts from the end. Negative indexing also stays correct if the expression is copied somewhere the length is not handy.

The crash happens on the first word, not partway through, which is worth noticing. An IndexError that fires immediately suggests the index expression is wrong in general, while one that fires after a few iterations suggests a specific element is unusual, such as an empty string.

Neither spelling handles an empty string, since both ""[-1] and ""[len("") - 1] raise. The empty case from lesson 3-1's checklist applies here too, and the fix would be a guard or a filter rather than a different index.

Hunt 3, a KeyError

The program crashes with a KeyError halfway through the order, and the spec says items missing from inventory should print 0 instead of crashing. Before the fix, the lookup was inventory[item].

inventory = {"apple": 4, "banana": 2}

for item in ["apple", "cherry", "banana"]:
    print(item, inventory.get(item, 0))

Output

apple 4
cherry 0
banana 2

KeyError: 'cherry' says the lookup crashed on a key that is not there, and since the spec says missing means 0, the square-bracket lookup is simply the wrong tool. dict.get(key, default) returns the default instead of raising, so inventory.get(item, 0) does exactly what the spec asks.

This is the one hunt where the crash is not a mistake in the usual sense. The code did what square brackets are defined to do, and the bug is a mismatch between the tool chosen and the behavior specified, which is why reading the spec is part of the diagnosis.

Note the third line still prints banana 2, which is the evidence that the loop now completes. A crash partway through leaves you unsure whether the remaining items would have worked, and that uncertainty is its own reason to fix rather than guess.

The default matters as much as the method. inventory.get(item) with no default returns None, which prints as cherry None and would then break any arithmetic downstream with the NoneType message from lesson 8-2.

Hunt 4, the append trap

The program crashes with a TypeError mentioning NoneType, which is the exact trap from lesson 8-2's special note. Before the fix, it read scores = scores.append(60).

scores = [88, 95, 70]
scores.append(60)
print(sorted(scores))

Output

[60, 70, 88, 95]

The message is TypeError: 'NoneType' object is not iterable, so sorted() received None, and the question to ask is which line turned scores into None. append() modifies the list in place and returns None, so assigning that return value back over scores destroys the list.

Dropping the assignment is the fix, leaving scores.append(60) on its own line. The list mutates in place and sorted(scores) gets a real list, which is the same lesson as the sort() case with a different method.

Compare the two messages carefully, because they differ. Subscripting None gives not subscriptable and iterating it gives not iterable, and both mean the same underlying leak. Recognizing the family rather than memorizing one sentence is what makes this fast to diagnose.

The rule generalizes beyond these two methods. append, sort, reverse, extend, insert, and clear all mutate and return None, while sorted, reversed, and a slice all return something new. Assigning the result of anything in the first group is nearly always a bug.

What hunt 1 illustrates about tracebacks

In hunt 1 the traceback pointed at the print line inside the loop, and the fix was correcting a name. The lesson is that the crash line is where the problem became fatal, and the trace's job is to start your search rather than end it.

The trace names the crash site precisely, and your hypothesis loop takes over from there. Sometimes the guilty code sits on the crash line itself, as with the misspelled call here, and sometimes the bad data came from a caller above, as with lesson 8-1's banana example. Either way the trace is the map and not the destination.

The four hunts in this lesson show the range:

HuntExceptionWhere the fix lived
1NameErroron the crash line, a misspelling
2IndexErroron the crash line, a wrong index
3KeyErroron the crash line, the wrong lookup tool
4TypeErrorone line above the crash

Hunt 4 is the important row. The assignment that created the None succeeded quietly, and the crash surfaced on the next line, so a reader who trusted the pointed-at line would have gone looking for a problem in sorted.

That is why the loop from lesson 7-1 follows the trace rather than replacing it. The trace gives you a precise observation, which is step 1, and the hypothesis about how the value got that way is still yours to form.