3  Debugging


Goals


What is Debugging?

As you’re probably already aware, debugging is as much a part of programming as writing code.

While tests can help identify & prevent errors– the errors themselves are inevitable, and all but the simplest programs are guaranteed to have bugs.

While sometimes you may see an error and immediately realize what caused it– “Oh! I forgot to update that method to take a third parameter!”, more often you find yourself saying “Wait… what?”

At any significant level of complexity, just staring at your code is not going to help you find the bug. You are going to need to explore what it is doing, following the reasoning in the code to see where it differs from your expectation or intention.

To illustrate this we’ll walk through three different levels of debugging:

Rubber Duck Debugging

The first level is just looking at your code and trying to reason about it.

You do this all the time when you encounter a syntax error:

File "syntax_err.py", line 1
    for x in [1, 2, :
                    ^
SyntaxError: invalid syntax

While that message may have mystified you at the beginning of your journey, at this point you can easily spot what is wrong, and make the fix.

Have you ever found that talking about your code with someone, even if they aren’t a programmer has helped you identify the error?

def count_odds(data):
    accum = 0
    for x in data:
        if x % 2 == 1:
            x += 1
    return accum

Do you see the bug?

If we talk through it would you?

We create an accumulator variable. Then we walk through the data one item at a time. If the number modulo two has a remainder of 1, it is odd. If it is odd, we add one to the… wait!

We are adding one to the variable, not the accumulator!

So where’s the duck?

rubber duck 006

rubber duck 006 - chan yiu hao from licensed CC-BY

This technique is named after a story in the book The Pragmatic Programmer by Andrew Hunt and David Thomas. The story goes that a programmer would carry around a rubber duck and explain their code to the duck. Often, in the process of explaining the code, the programmer would realize what was wrong.

You may find that it doesn’t matter if there’s another person there: talking to a rubber duck, pet, or plant may be beneficial.

And yes, it helps if it is out loud.

This helps build an important skill & should not be overlooked, but obviously won’t solve all your problems.

Interactive Debugging

Finally, we have interactive debugging.

This allows you to interact with the program as it runs, pausing the execution to inspect the variables and can let you literally watch the program run line-by-line to see where the error occurs.

Python has a built-in debugger named pdb. We’ll use ipdb, which embeds ipython and has the same benefits that ipython does over the regular python REPL. Most of what we’ll see works for regular PDB as well.

Running the Debugger

You can run the debugger from the command line, which would look like:

$ python -m pdb my_script.py
$ python -m pdb -m my_module

The first form executes a script, while the second runs the pdb module, telling it in turn to run my_module.

If we use ipdb it will need to be installed. With uv that would look like:

$ uv add ipdb  # if not already installed in this package
$ uv run python -m ipdb -m my_module

PDB Commands

Once the program is run this way, it will let you step through the lines one line at a time.

A few of the most common commands and their 1-character shortcuts:

  • next - step to next line (aka step over)
  • step - step into a function call (aka step into)
  • return - continue until the current function returns (aka step out)
  • continue - continue until the next breakpoint
  • list - list the current line of code
  • print - print the value of a variable
  • quit - quit the debugger
  • ?/help - show a list of commands
$ python -m ipdb debugging/example1.py

Breakpoints

Starting from the beginning can be tedious, usually you will want to start the debugger sometime after the program starts, but closer to the trouble area you’re debugging.

Python has a built in statement for this, breakpoint()

Note

You may see older code use import pdb; pdb.set_trace() instead. breakpoint is more flexible and available after Python 3.7.

This will pause execution at that point and allow you to step through your code.

Visual Studio Code Debugger

Finally, most editors, including Visual Studio Code have debugger integration.

To use VS Code’s integration, you’ll need to add a launch.json file to your project.

This file tells VS Code how to run your program and what to do when you start debugging.

You can create a launch.json file by clicking on the debug icon in the sidebar and then clicking the gear icon to create a new launch configuration.

Create launch.json

This will create a launch.json file in your .vscode directory. You can then edit this file to configure your debugger.

Here’s an example launch.json file:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Current File",   // this is the name that will show up in the dropdown
            "type": "python",
            "request": "launch",
            "program": "${file}",             // this tells VSCode to run the current file
            "arguments": ["arg1", "arg2"],    // if your program requires arguments you'll need to add this
            "console": "integratedTerminal",
            "justMyCode": true                  // this tells VSCode to not step into external libraries
        }
    ]
}

Further Exploration

If you are using pdb/ipdb, you may find this Debugging Cheat Sheet helpful.