Debugging: reading a traceback & stepping with breakpoint()
Read a traceback bottom-up, pause code with breakpoint(), and pick print vs stepping.
The question every interview asks
"How do you debug?" comes up in nearly every screen, and the weak answer is "I add print statements until I find it." That is not wrong, it is incomplete. A strong answer has three parts: read the traceback properly so you start at the right line, use prints when the question is what a value was, and use a real debugger when the question is how the program got here at all. This lesson covers all three.
The debugger commands below run in a terminal, not in the editor on this page. The browser sandbox has no interactive prompt, so breakpoint() would have nothing to talk to. The graded exercises pull apart traceback text instead, which is the half of the skill that travels furthest: a crash report from CI or an on-call channel arrives as text, and you have to answer "what broke" and "where" from it with no debugger anywhere in reach.
A traceback is a stack, printed oldest first
Traceback (most recent call last):
File "report.py", line 21, in <module>
print(average(readings))
File "report.py", line 14, in average
return total(values) / len(values)
File "report.py", line 9, in total
return sum(values)
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Read it bottom-up.
- The last line is the error.
TypeErroris the exception type and everything after the colon is its message. Start here every single time: it usually names the fix, and it costs you nothing to read. - The frame directly above it is where execution stopped.
line 9, in total, at the statementreturn sum(values). That is the innermost frame, and it prints last, which is exactly what "most recent call last" in the header is telling you. - Every frame above that is the caller of the one below it.
totalwas called fromaverageat line 14, which was called from the top level of the file (<module>) at line 21. That chain is the route a bad value took to reach the line that finally choked on it.
The innermost frame is where the program died. It is not always where the bug lives. Here sum(values) is perfectly reasonable code: the real defect is whoever put a string into readings, several frames up.
Pausing the program with breakpoint()
A print tells you what one value was, at one line you had to guess in advance. A debugger stops the program and lets you ask anything, at any point, without editing and re-running. Since Python 3.7 you get one with a single line, no import required:
def average(values):
breakpoint() # execution pauses here and hands you a prompt
return sum(values) / len(values)
Run the file normally and you land at a (Pdb) prompt with the program frozen mid-call, every local variable still alive.
| At the (Pdb) prompt | What it does | Reach for it when |
|---|---|---|
| p values | Evaluates any expression in the paused frame | You want a value, or p type(values[0]) to see what it really is |
| n (next) | Runs the current line and stops on the next one in this function | You trust the calls on this line and want the result |
| s (step) | Same, but steps INTO the call instead of over it | One of the calls on this line is your suspect |
| l (list) | Shows the source around where you are paused | You have lost track of which line you are on |
| c (continue) | Runs on until the next breakpoint or the end | You have seen enough here |
| q (quit) | Stops the program | You already know the fix |
When a print is genuinely the better tool
Print debugging is not the beginner version of a debugger, it is a different instrument. Reach for a print when the question is "what was this value" and you already know the line to ask at, when you need a pattern across thousands of iterations rather than one frozen moment, or when the code runs somewhere nothing can attach to it: CI, a container, a scheduled job at 3am.
Reach for breakpoint() when you do not know where to look yet, so any print is a guess. When the state is big and you want to poke at six values, because each extra print costs a whole re-run. Or when the question is not a value at all but the route: how did control reach this line.
The shape of the exercises
Both exercises take one traceback as a single string and answer a question about it. Split it into lines with trace.strip().split("\n"). The last line is the error, and every line that starts with File once you strip its indentation is a frame. Apply asks for the exception type, which is the "what". Practice asks for the line number of the innermost frame, which is the "where", the first place you would open your editor.
Interview nuance: the shape of a strong answer to "how do you debug" is three moves. Read the last line of the traceback for what broke. Walk up the frames to the last one in code you own, which is usually where the bad value entered. Then pick a tool by the question you have left: print for "what was this value at this line", breakpoint() for "how did we get here and what else is true right now". Saying only "I add prints" reads as never having debugged something you could not guess.
trace = """Traceback (most recent call last):
File "report.py", line 21, in <module>
print(average(readings))
File "report.py", line 9, in total
return sum(values)
TypeError: unsupported operand type(s) for +: 'int' and 'str'"""
lines = trace.strip().split("\n")
print(lines[-1]) # the error itself
print(lines[-1].split(":")[0]) # just the exception typeApply
Your turn
The task this lesson builds to.
Implement exception_type(trace): return the exception type named at the end of a traceback.
The last line of a traceback is the error, like ZeroDivisionError: division by zero. Return only
the type name, "ZeroDivisionError". A last line carrying no message at all, like
StopIteration, returns that whole line.
3 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
An overnight job died and the on-call channel holds nothing but the traceback somebody pasted into it. Before you can open a file you need the one number that says where to look.
Implement crash_line(trace): return the line number of the innermost frame, which is the LAST
line of the traceback that starts with File once its indentation is stripped. A frame line looks
like File "report.py", line 9, in total. Return 0 when the report was truncated and carries
no frame at all.
3 hints and 4 automated checks are waiting in the workspace.