Chapters
Python114 chapters

Beyond the basicsChapter 98 of 114

Debugging

Read a traceback, then find the problem with a debugger instead of guessing.

Read the traceback from the bottom

The last line is what went wrong. The lines above it are how you got there, and the frame nearest the bottom is usually yours:

Python
import traceback

def level_two(values):
    return values[10]

def level_one(values):
    return level_two(values)

try:
    level_one([1, 2, 3])
except IndexError:
    report = traceback.format_exc().strip()

lines = report.splitlines()
print(lines[0])
print(lines[-1])
print("level_one" in report, "level_two" in report)

Output

Traceback (most recent call last):
IndexError: list index out of range
True True

The example prints the first and last lines because everything between them — the frames, and the ~~~^^^ markers under the failing expression — is formatted slightly differently by each Python version.

Read it bottom up. The last line is what: an index was out of range. The frame just above it is where: values[10] in level_two. The frames above that are how you got there. Modern Python underlines the exact sub-expression that failed, which often ends the investigation right there.

There is nothing wrong with a print. Make it say which value it is:

Python
total = 0
for n in [1, 2, 3]:
    total += n
    print(f"{n=} {total=}")

Output

n=1 total=1
n=2 total=3
n=3 total=6

The = inside an f-string prints the expression and its value, so a renamed variable cannot leave you reading a stale label.

repr shows what print hides

Python
value = "42 "
print(f"looks fine: [{value}]")
print("really is: ", repr(value))
print(value == "42")
print(int(value) == 42)

Output

looks fine: [42 ]
really is:  '42 '
False
True

A trailing space is invisible until you ask for the repr. When something looks right and behaves wrong, this is the first thing to try.

The debugger

breakpoint() stops the program and gives you a prompt. From there you can inspect anything and step through:

Python
def total(values):
    running = 0
    for value in values:
        breakpoint()
        running += value
    return running

total([1, 2, 3])

The commands worth memorising:

CommandDoes
nnext line, stepping over calls
sstep into the call
ccontinue until the next breakpoint
llist the code around here
p nameprint a value
pp namepretty-print it
wwhere am I: the call stack
u / dmove up or down a frame
qquit

Any other input is evaluated as Python, so you can call functions and inspect objects while stopped.

breakpoint() beats import pdb; pdb.set_trace(): it is shorter, and setting PYTHONBREAKPOINT=0 disables every one without editing the code.

bash
PYTHONBREAKPOINT=0 python script.py
python -m pdb script.py          # start under the debugger

Post-mortem

After a crash, pdb.post_mortem() drops you into the frame where it happened, with all its variables intact:

Python
import pdb

try:
    risky()
except Exception:
    pdb.post_mortem()

That beats adding a breakpoint and running again, especially when the failure is intermittent.

Assert what you believe

Python
def average(values):
    assert values, "average of an empty list"
    return sum(values) / len(values)

print(average([1, 2, 3]))

try:
    average([])
except AssertionError as problem:
    print("AssertionError:", problem)

Output

2.0
AssertionError: average of an empty list

An assertion turns "this should never happen" into a loud failure at the point where the assumption broke, rather than a confusing error three functions later. Remember they vanish under -O, so never use them to validate input.

Narrow it down before you look

Debugging is a search. Every step should halve what is left:

  1. Reproduce it reliably. An intermittent bug is not yet a bug you can fix.
  2. Find the smallest input that still fails. Usually this alone reveals it.
  3. Bisect the code. Print or break halfway, and see which side is wrong.
  4. Check your assumptions with repr. Types and whitespace, mostly.
  5. Write the test that would have caught it, then fix it.

If you have been staring for twenty minutes, the bug is somewhere you are certain it is not. Print the thing you are sure about.

Test yourself

2 questions

Which line of a traceback tells you what went wrong?

Show the answer

The last one — Read it bottom up: the last line is what, the frame above it is where, and the rest is how you got there.

Why is breakpoint() better than import pdb; pdb.set_trace()?

Show the answer

It is shorter, and PYTHONBREAKPOINT=0 disables every one without editing code — You can also point PYTHONBREAKPOINT at a different debugger entirely.

Next chapter

SQLite

A real database in a single file, with no server, built into Python.