Your program prints 6 and you expected 10. You stare at the code. It looks right. The trouble is that your eyes see the text of the program, but the computer runs it one tiny step at a time, and the answer is hiding in those steps. A debugger is a tool that lets you slow the program down and watch them.

The idea: pause, look, step

A debugger does three things. It lets you set a breakpoint, a marker that says "pause here". It lets you step, which means run exactly one line and pause again. And it lets you inspect the variables, the named boxes that hold the program's values, at every pause. Nothing is changed by looking, so you can look as often as you like.

A function with a bug

This Python function should add up the whole numbers from 1 to n. For n = 4 that is 1 + 2 + 3 + 4 = 10.

def sum_to(n):
    total = 0
    for i in range(1, n):
        total += i
    return total

print(sum_to(4))   # prints 6, not 10

Instead of guessing, step through it below. Press Step and watch the highlighted line and the variables table. Ask yourself after each step: is this what I expected?


    
variablevalue

Each step runs one line. The highlighted line is the one that is about to run. Values shown were recorded from a real run of this Python code.

What you should have noticed

With n = 4, the variable i takes the values 1, 2 and 3, then the loop ends. It never becomes 4. The reason is that range(1, n) stops before n: Python's range includes the start and excludes the end. To include n you write range(1, n + 1). Switch the "code" menu to "with the fix" and step again: now i reaches 4 and the answer is 10.

Notice how the debugger turned a vague feeling ("the answer is wrong") into a precise question ("why did the loop stop at 3?"). That is the real skill. Finding the bug is mostly about narrowing down where the program stopped matching your expectation.

Doing this on your own computer

Python ships with a text debugger called pdb. Since Python 3.7 you can pause anywhere by calling the built-in breakpoint():

def sum_to(n):
    total = 0
    breakpoint()          # the program pauses here
    for i in range(1, n):
        total += i
    return total

When it pauses you get a (Pdb) prompt. A few commands cover most needs: n runs the next line, s steps into a function call, p total prints a variable, c continues until the next breakpoint, and q quits.

Editors such as VS Code have the same ideas with buttons. You click in the margin next to a line number to set a breakpoint, run in debug mode, and a side panel shows your variables. "Step over" runs the line without entering functions it calls, and "step into" follows the call.

Debugger or print?

Printing values with print() is a perfectly good way to look inside a program, and many experienced people use it daily. A debugger shines when you do not yet know which value to print, because you can pause once and look at everything in scope. Use whichever gets you to the answer faster.

A habit worth keeping

Before you press Step, say out loud what you expect the next value to be. The moment the debugger disagrees with you is the moment you have found the bug, or found a wrong belief about how the language works. Both are valuable.