Skip to main content

Debugging: reading a traceback & stepping with breakpoint()

Level 1: Level 1: Foundationsmedium13 mindebuggingtracebackserrorserror-handling

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.

  1. The last line is the error. TypeError is 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.
  2. The frame directly above it is where execution stopped. line 9, in total, at the statement return 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.
  3. Every frame above that is the caller of the one below it. total was called from average at 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.

Check yourself
The innermost frame of your traceback points at a file inside site-packages, deep in a library you did not write. Where is the bug most likely to be?

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.

Table
Only two of these do investigative work. p answers what a value is right now, and n versus s decides whether you watch a call from outside or from inside. The rest is navigation.
At the (Pdb) promptWhat it doesReach for it when
p valuesEvaluates any expression in the paused frameYou 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 functionYou trust the calls on this line and want the result
s (step)Same, but steps INTO the call instead of over itOne of the calls on this line is your suspect
l (list)Shows the source around where you are pausedYou have lost track of which line you are on
c (continue)Runs on until the next breakpoint or the endYou have seen enough here
q (quit)Stops the programYou already know the fix
Only two of these do investigative work. p answers what a value is right now, and n versus s decides whether you watch a call from outside or from inside. The rest is navigation.
Check yourself
You are paused on return total(values) / len(values) and you suspect total is computing the wrong number. Do you press n or s?

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.

Check yourself
A nightly job processes 5000 rows and raises a KeyError on roughly one of them. It runs unattended at 3am. What do you do first?

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.

Worked example (Python)
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 type

Apply

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.