Skip to main content

Debugging as a method

A wrong answer with no message is found by experiment, not by staring: reproduce it, shrink the input, predict each line, look, and halve the suspect region. Worked on a median with two planted bugs, with prints and the debugger.
P01.8.3L0 Foundation25 minIn review

Before this lesson

P01.8.2 Handling exceptions: try, except and raise

After this lesson you can

  • Run the debugging loop: reproduce the failure, shrink the input, predict what each line should give, look, and halve the region where prediction and fact part.
  • Look inside a running program with a trace table, temporary prints and the debugger’s next, step and p commands.
  • Explain why halving finds one bad step among 8 in three looks, and among 1,000 in ten.
  • Find and repair the two bugs of a planted median, and check the repair against statistics.median.

In review. The maths and code on this page are checked by automated tests on every change, but an independent reader has not signed it off yet. Spot a mistake or a confusing step? Tell us.

The question

statistics.median says the middle score of the study table is 60. The median you wrote yourself says 80. There is no traceback: the program ran to the end and printed a wrong number. Where do you even start looking, and how do you know when you have found it?

From the last lesson

1. When does the else block of a try statement run?

Show answer

Only when the try block raised no exception.

2. Why is a bare except: a bad idea?

Show answer

It catches every exception, including those caused by real bugs such as a typo, so the bugs are hidden instead of reported.

Intuition

A bug is a mistake in a program. Debugging is finding and removing it. Staring at the code and hoping is the slow way. The fast way is an experiment, repeated: make a prediction about the program, look at what it really does, and use the difference to rule out part of the code.

A flow chart that starts at a crash or a wrong answer. Four steps follow: 1, reproduce it, the same failure on every run; 2, shrink the input to the smallest one that still fails; 3, predict what each line should give; 4, look at the line halfway through the suspects. A diamond asks whether prediction and fact agree. True leads to the bug is later, keep the second half; False leads to the bug is here or earlier, keep the first. A second diamond asks whether only one suspect is left. False loops back to step 4, halve again; True leads to 5, fix it, then rerun every case.
P01.8.3-F1

Debugging as an experiment repeated. Each look at the halfway point rules out half of the lines still under suspicion.

The loop has one idea at its heart. Before you look, you must know what you expect to see. A printed value means nothing until you have predicted it. Where the prediction and the fact first part company, the bug is close.

Build it up

Here is a median with a bug in it. It runs without an error.

import statistics


def median(values):
    middle = len(values) // 2
    return values[middle]


scores = [55, 65, 80, 40, 60]
print(median(scores), statistics.median(scores))
80 60

Step 1. Reproduce it. Find an input that makes the program fail every time, and write it down. Here it is easy: the scores give 80 on every run, and the right answer is 60. A failure that comes and goes, perhaps because of random numbers, is first made steady, for example with a fixed random.seed (P01.5.1).

Step 2. Shrink the input. The fewer values, the less there is to follow. Three scores are enough to show the bug: the median of 80, 40 and 60 is 60, the middle one once they are in order.

print(median([80, 40, 60]))
40

Step 3. Predict. Before running anything, write down what each line should give for [80, 40, 60]. Line 2: middle is \({3 // 2} = 1\). Line 3: the median is the value at position 1, counting from 0, of the sorted list [40, 60, 80], which is 60.

Step 4. Look. Now look at what the program really does, with one of three tools.

  • A trace table by hand, as in every lesson so far: one row per line, with the values after it.
  • Temporary prints: a print that shows the values at a chosen line, removed once the bug is found. Think Python calls such code scaffolding.
  • The debugger, which pauses the program and lets you run it one line at a time.
def median(values):
    middle = len(values) // 2
    print('  values:', values, ' middle:', middle)    # temporary
    print('  values[middle]:', values[middle])        # temporary
    return values[middle]


print(median([80, 40, 60]))
  values: [80, 40, 60]  middle: 1
  values[middle]: 40
40

Line 2 agrees with the prediction: middle is 1. Line 3 does not: it gives 40, not 60. The fact that parts from the prediction is plain to see in the print: values is not in order. The bug is on line 3, which takes the middle of the list as it was given.

Step 5. Halve. In a function of three lines, one look is enough. In a program of hundreds of lines, you choose where to look: halfway through the lines still under suspicion.

Eight steps of a program in a row: read the file, drop the header, split the lines, take the scores, make numbers, sort them, find the middle, print it. Then three looks, each a row of eight cells with the suspects tinted purple and a thick bar at the point looked at. Look 1, suspects 1 to 8, looks after step 4: predicted and found are the same list of text scores, a tick, the bug is later. Look 2, suspects 5 to 8, looks after step 6: predicted 40, 55, 60, 65, 80 but found 55, 65, 80, 40, 60, a cross, the bug is here or earlier. Look 3, suspects 5 to 6, looks after step 5: predicted and found agree, a tick. One suspect is left after three looks: step 6, which does not sort.
P01.8.3-F2

Halving the suspects. Eight steps need only three looks: 8, then 4, then 2, then 1. The bug is in step 6, which does not sort.

If the values agree with the prediction at the halfway line, everything before it is cleared, and the bug is in the second half. If they disagree, the bug is at that line or before it. Either way, half the suspects are gone.

The formal statement

NoteThe debugging method
  1. Reproduce the failure: find an input that makes it happen on every run.
  2. Shrink the input to the smallest one that still fails.
  3. Predict what each line should give for that input.
  4. Look at the line halfway through the suspects, with a trace table, a print or the debugger.
  5. Halve: if prediction and fact agree there, keep the later half; if not, keep the earlier half, that line included. Go back to step 4 until one suspect is left.
  6. Fix that line, then rerun every case you have, not just the one that failed.

How many looks? Each look keeps at most half of the suspects, rounded up. Starting from \(n\) suspect lines, after \(k\) looks at most

\[ \frac{n}{2^k}\ \text{(rounded up)} \]

remain. One suspect is left once \({2^k \ge n}\), so the number of looks is the smallest \(k\) with \({2^k \ge n}\).

  • For \({n = 8}\): \({2^3 = 8}\), so three looks, as in the figure.
  • For \({n = 1000}\): \({2^9 = 512}\) is too small and \({2^{10} = 1024}\) is enough, so ten looks.

Reading 1,000 lines one by one would take up to 1,000 looks. MIT’s 6.100L gives the same advice for print statements: “use bisection method”, with a print halfway through the code and the values deciding which half the bug may be in.

The debugger. Put the call breakpoint() in a program and run it. When Python reaches that line it pauses and shows the prompt (Pdb), the debugger of the module pdb. Each command is one word.

  • next (or n) runs the current line and stops at the next line of the same function.
  • step (or s) does the same, but stops inside a function that the line calls.
  • p expression prints the value of any expression, such as p middle.
  • continue (or c) runs on until the next breakpoint or the end; quit (or q) stops the program (Python 3.14 and later first ask you to confirm with y).

Here is a session with the buggy median. The arrow -> marks the line that is about to run.

median_bug.py
def median(values):
    middle = len(values) // 2
    return values[middle]


scores = [55, 65, 80, 40, 60]
breakpoint()
result = median(scores)
print('median', result)
python median_bug.py
> median_bug.py(7)<module>()
-> breakpoint()
(Pdb) next
> median_bug.py(8)<module>()
-> result = median(scores)
(Pdb) step
--Call--
> median_bug.py(1)median()
-> def median(values):
(Pdb) next
> median_bug.py(2)median()
-> middle = len(values) // 2
(Pdb) next
> median_bug.py(3)median()
-> return values[middle]
(Pdb) p middle
2
(Pdb) p values[middle]
80
(Pdb) p sorted(values)
[40, 55, 60, 65, 80]
(Pdb) continue
median 80

The session was copied from Python 3.15, with the folder cut from the file name. Python 3.12 pauses one line later, on the line after breakpoint(), so there the first next is not needed; from then on the session is the same. The debugger needs a keyboard, so this page shows it rather than runs it.

Worked example

The median above has two planted bugs. Here is the whole hunt.

Two small trace tables with columns line, code, predicted and found. Part 1, median of the five scores with both bugs: line 2, middle = len(values) // 2, predicted 2 and found 2, a tick; line 3, return values[middle], predicted 60 and found 80, a cross, shaded purple. Part 2, median of the first four scores after sorting is added: ordered = sorted(values) gives 40, 55, 65, 80 as predicted, a tick; middle = len(ordered) // 2 gives 2 as predicted, a tick; return ordered[middle] was predicted to give 60.0 but gives 65, a cross. Notes say the middle of the list as given is not the middle of the sorted list, and that an even count has two middle values and the code takes only the upper one.
P01.8.3-F3

The trace of each run, predicted beside found. In each part the first row where the two part company holds the bug.

Bug 1: it forgets to sort. On the five scores, middle is \({5 // 2} = 2\), as predicted. But values[2] is 80, the third score as given, not the middle of the sorted list. The repair is one line, ordered = sorted(values), and taking the middle of ordered instead.

def median(values):
    ordered = sorted(values)
    middle = len(ordered) // 2
    return ordered[middle]


print(median(scores))
print(median(scores[:4]))
60
65

The first number is now 60, right. One passing case is not a proof, so try another kind of case: an even count, the first four scores.

Bug 2: it takes the upper middle of an even count. By hand, the sorted four are 40, 55, 65 and 80. There are two middle values, 55 and 65, and the median is their mean, \({(55 + 65) / 2} = 60.0\). The code gives 65.

Two lists drawn as rows of numbered slots, with positions 0 to 4 and 0 to 3 above them. Five sorted scores, 40, 55, 60, 65, 80: the single middle slot at position 2, holding 60, is filled yellow; middle = 5 // 2 = 2 and ordered[2] is 60, the middle, with a tick. Four sorted scores, 40, 55, 65, 80: the two middle slots at positions 1 and 2, holding 55 and 65, are filled yellow; middle = 4 // 2 = 2, and ordered[2] is 65, only the upper one, with a cross; (ordered[1] + ordered[2]) / 2 = (55 + 65) / 2 = 60.0, with a tick.
P01.8.3-F4

An odd count has one middle slot. An even count has two, at positions middle - 1 and middle, and the median is their mean.

The repair handles the even case on its own path, with % from P01.2.3. Then compare it with the library, statistics.median, on both lists.

def median(values):
    if len(values) == 0:
        raise ValueError('median of an empty list')
    ordered = sorted(values)
    middle = len(ordered) // 2
    if len(ordered) % 2 == 1:
        return ordered[middle]
    lower = ordered[middle - 1]
    upper = ordered[middle]
    return (lower + upper) / 2


print(median(scores), statistics.median(scores))
print(median(scores[:4]), statistics.median(scores[:4]))
60 60
60.0 60.0

Both agree. The guard on an empty list was added on the way, with the same clear message as mean in P01.8.2. The library also refuses an empty list: it raises StatisticsError, which is a kind of ValueError.

Code

One more experiment makes the repair trustworthy: compare it with the library on a thousand random lists of random lengths, odd and even. The seed makes every run the same.

import random

random.seed(8)
disagree = 0
for trial in range(1000):
    size = random.randint(1, 9)
    values = []
    for k in range(size):
        values.append(random.randint(0, 100))
    if median(values) != statistics.median(values):
        disagree += 1
print('lists where the two medians disagree:', disagree)
lists where the two medians disagree: 0

The repaired median now joins mean in stats_tools.py, the module begun in P01.5.2, where P01.8.4 will test both.

stats_tools.py
"""Small statistics for the study table."""


def mean(values):
    if len(values) == 0:
        raise ValueError('mean of an empty list')
    return sum(values) / len(values)


def median(values):
    if len(values) == 0:
        raise ValueError('median of an empty list')
    ordered = sorted(values)
    middle = len(ordered) // 2
    if len(ordered) % 2 == 1:
        return ordered[middle]
    lower = ordered[middle - 1]
    upper = ordered[middle]
    return (lower + upper) / 2


def predict(hours, slope=9, intercept=33):
    return slope * hours + intercept
from stats_tools import mean, median

print(mean(scores), median(scores), median(scores[:4]))
60.0 60 60.0

Explore

This module’s interactive, the traceback explorer, lives in P01.8.1. Use it as a debugger you cannot break: before each press of Step, predict the line that will light up and the names in the newest frame, then look.

Pitfalls

Wrong, with a cross: a one-row table, the case 55, 65, 80, 40, 60, worked by hand 60, median 60, a tick; a note says that after the first fix this passes, yet the even-count bug is still there. Right, with a tick: three rows. The five scores give 60 as worked by hand, a tick; the four scores 55, 65, 80, 40 should give 60.0 but give 65, a cross, shaded purple; the single score 55 gives 55, a tick. A note says an odd count, an even count and a single value show the second bug at once.
P01.8.3-F5

Wrong beside right. One passing case after a fix proves little. A case of every kind (an odd count, an even count, a single value) shows the second bug straight away.

“It works on my example, so it is fixed.”

After the first fix, the five scores give 60 and the median looks repaired. It is not: an even count still gives the wrong answer. After every fix, rerun every kind of case, not only the one that failed. The next lesson makes the computer do this for you.

“Change things until the output looks right.”

Editing at random, several things at once, can make one output right by accident and break another. Change one thing at a time, for a reason you can state, and rerun the same input after each change. Software Carpentry puts it as “change one thing at a time, for a reason”.

“Prints and breakpoints can stay; they do no harm.”

A forgotten print fills the output of every later run, and a forgotten breakpoint() stops the program and waits for a keyboard that a server running your training job does not have. Remove scaffolding as soon as the bug is found.

Check yourself

Recall

1. What are the steps of the debugging method, in order?

Show answer

Reproduce the failure, shrink the input, predict what each line should give, look at the line halfway through the suspects, halve the region where prediction and fact part, and repeat; then fix and rerun every case.

2. Why must you predict a value before you look at it?

Show answer

Without a prediction, a printed value cannot be called right or wrong, so it rules nothing out.

3. In the debugger, what is the difference between next and step?

Show answer

Both run the current line. step stops inside a function that the line calls; next runs the call through and stops at the next line of the same function.

4. How many looks does halving need to find one bad line among 1,000?

Show answer

Ten, because \({2^{10} = 1024}\) is the first power of 2 that reaches 1,000.

5. What were the two bugs of the planted median?

Show answer

It did not sort the values before taking the middle one, and for an even count it returned the upper of the two middle values instead of their mean.

Exercises

1. ★ By hand, what does the buggy median (no sorting, upper middle) return for [3, 9, 1, 5], and what is the true median?

Show solution

middle is \({4 // 2} = 2\) and values[2] is 1, so it returns 1. Sorted, the list is 1, 3, 5, 9; the two middle values are 3 and 5, so the true median is \({(3 + 5) / 2} = 4.0\). Both bugs show up at once.

2. ★★ A program of 64 lines prints a wrong total. You print the total after line 32 and it agrees with your prediction; after line 48 it does not; after line 40 it agrees. Which lines are still suspects, and how many more looks do you need at most?

Show solution

Agreement after 32 clears lines 1 to 32. Disagreement after 48 puts the bug in 33 to 48. Agreement after 40 clears 33 to 40. So lines 41 to 48 are left: 8 suspects, which need at most 3 more looks (8, then 4, then 2, then 1). In all, \({2^6 = 64}\), so six looks always suffice.

3. ★★★ This function should return the largest score but returns the smallest for [55, 65, 80, 40, 60]. Use predict-and-look on paper to find the bug.

def largest(values):
    best = values[0]
    for value in values:
        if value < best:
            best = value
    return best
Show solution

Predict best after each pass: 55, 65, 80, 80, 80. Look, by tracing the code: the first pass compares 55 with 55 and keeps 55; the second asks whether 65 < 55, which is False, so best stays 55, while the prediction says 65. Prediction and fact part at the first pass that should change best, so the bug is in the comparison. It should be value > best. With that change the passes give 55, 65, 80, 80, 80 and the function returns 80. The trace as written gives 55, 55, 55, 40, 40 and returns 40, the smallest.

Summary card

Predict, look, and halve the region where prediction and fact part.

The anchor flow chart again: reproduce, shrink, predict, look at the halfway line; if prediction and fact agree keep the later half, otherwise the earlier; repeat until one suspect is left, then fix and rerun every case.
P01.8.3-F1

Debugging as an experiment.

  • Before looking: reproduce the failure and shrink the input.
  • Tools: a trace table, temporary prints, breakpoint() with next, step, p and continue.
  • Halving: \(n\) suspects need the smallest \(k\) with \({2^k \ge n}\) looks; 1,000 lines, ten looks.
  • After the fix: rerun every kind of case, not just the one that failed.

Sources and next steps

Sources. Each was opened and checked against this lesson on 12 November 2026.

  • Software Carpentry, Programming with Python: Debugging. The sections “Know What It’s Supposed to Do”, “Make It Fail Every Time” and “Make It Fail Fast”, whose divide-and-conquer advice is to “check whether things are OK in the middle, then concentrate on either the first or second half”, and “Change One Thing at a Time, For a Reason”.
  • Ana Bell, MIT OpenCourseWare 6.100L, Fall 2022, Lecture 12: List Comprehension, Functions as Objects, Testing, Debugging. The debugging steps as the scientific method (“form hypothesis”, “repeatable experiments”, “pick simplest input to test with”) and print statements with the bisection method.
  • Python Software Foundation, pdb, The Python Debugger: breakpoint(), the (Pdb) prompt, and the commands step, next, p, continue and quit; and statistics.median: the middle value for an odd count, the mean of the two middle values for an even count, and StatisticsError, a subclass of ValueError, for empty data.
  • Python Software Foundation, What’s New in Python 3.13, pdb: breakpoint() now enters the debugger at once instead of on the next line; and What’s New in Python 3.14, pdb: quitting a debugger started this way first shows a confirmation prompt.
  • Allen B. Downey, Think Python, 3rd edition, chapter 6. Section 6.4 names temporary code “scaffolding”; section 6.10 suggests prints at the start and end of a function to make the flow of execution visible.

Next. P01.8.4 First tests turns the cases of this lesson into tests that rerun themselves.

This lesson unlocks

P01.8.4 First tests

Your progress is saved in this browser only. No account, no tracking.
Back to top