tools/debugging-for-beginners-step-through-code-to-find-an-off-by-one.md

Debugging for beginners: step through code to find an off-by-one

Tools · 3 min read ·

Your code prints 6 and you expected 10. A debugger lets you slow the program down, run one line at a time, and watch every variable change, so you can see exactly where reality stops matching your expectation.

Step through a tiny Python function in the page, spot the off-by-one hiding in range(1, n), then learn how to do the same on your own computer with breakpoint() and your editor.

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?

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.

$ ls tools/

see all →
tools/gitignore-how-git-decides-which-files-to-skip-and-the-one-rule-that-su.md Gitignore: how Git decides which files to skip, and the one rule that surprises everyone Why does a file show up in git status when you wanted it hidden, and why does a file you ignored sometimes stay tracked anyway? Five short… 5 min read · 27 Oct tools/pipes-how-the-in-a-terminal-turns-small-commands-into-one-big-answer.md Pipes: how the | in a terminal turns small commands into one big answer How do you find the most popular page in a file of 15 million visits? In a terminal, you join small programs with the vertical bar `|`… 5 min read · 23 Oct tools/git-branches-why-making-one-doesn-t-copy-your-project.md Git branches: why making one doesn't copy your project When someone says "make a branch for that," it sounds like Git must copy your whole project. It doesn't. A branch is just a name stuck on… 4 min read · 20 Oct tools/git-staging-area-why-saving-your-work-takes-git-add-and-git-commit.md Git staging area: why saving your work takes git add and git commit Why does saving your work in Git take two commands, `git add` and `git commit`? Because a change travels through three places: your working… 4 min read · 17 Oct tools/path-why-a-terminal-says-command-not-found-for-a-program-you-installed.md PATH: why a terminal says command not found for a program you installed You installed a tool, but the terminal says `command not found`, or it runs an older version. The cause is almost always `PATH`: a short… 5 min read · 14 Oct tools/git-merge-vs-rebase-same-code-two-different-histories.md Git merge vs rebase: same code, two different histories Git can combine two lines of work in two ways. Merge ties them together with a new commit and leaves every old commit alone. Rebase replays… 4 min read · 11 Oct