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:
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.
print debugging, done properly
There is nothing wrong with a print. Make it say which value it is:
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
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:
def total(values):
running = 0
for value in values:
breakpoint()
running += value
return running
total([1, 2, 3])The commands worth memorising:
| Command | Does |
|---|---|
n | next line, stepping over calls |
s | step into the call |
c | continue until the next breakpoint |
l | list the code around here |
p name | print a value |
pp name | pretty-print it |
w | where am I: the call stack |
u / d | move up or down a frame |
q | quit |
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.
PYTHONBREAKPOINT=0 python script.py
python -m pdb script.py # start under the debuggerPost-mortem
After a crash, pdb.post_mortem() drops you into the frame where it happened, with all its variables intact:
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
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:
- Reproduce it reliably. An intermittent bug is not yet a bug you can fix.
- Find the smallest input that still fails. Usually this alone reveals it.
- Bisect the code. Print or break halfway, and see which side is wrong.
- Check your assumptions with
repr. Types and whitespace, mostly. - 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 questionsWhich 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.
SQLite
A real database in a single file, with no server, built into Python.