Debugging Python code, for many of us, comes down to adding print statements and trying to reason what’s going on based on the output. It’s not ideal, but for some reason, people just don’t want to touch the Python debugger.
Using print to debug code
If you’re one of those people that use print for debugging, I’m not judging you. Learning Python is hard. Programming is hard. Life is hard. I understand that learning yet another thing seems tiresome if you’re struggling already. And adding that print statement to your code… it’s just easier. A little confession: I still do it all the time as well. It’s not as bad as some make it seem.
For some reason, people just don’t want to touch the Python debugger
Me.
Having said that, I’m going to teach you a little trick that will be just as easy as adding print statements to your code. However, this trick gives you infinite debugging powers and will allow you to conquer the world, should you need to.
Setting a Python debugger breakpoint
You don’t need an IDE to debug your code properly. This trick always works, even in the simplest editor, and even in Jupyter Notebooks.
The trick comes down to this: setting a so-called breakpoint using the instruction breakpoint(). You can insert it in your code, no matter what editor or IDE you’re using, at the point where you would normally put in that pesky print statement.
So what happens when you do so? Let’s find out! Create a simple program, like the one below:
def add_two(x):
return x + 2
for i in range(1, 10):
a = add_two(i)
breakpoint()
print(a)Code language: Python (python)
When you run this, and it doesn’t matter if you do so in the REPL, from the command-line, or in your IDE, you are dropped into the Python debugger called PDB. But now what?
Using the Python debugger
When I start the little program above, this is what happens:
$ python3 breakpointing.py
> /home/erik/breakpointing.py(6)<module>()
-> breakpoint()
(Pdb)Code language: Python (python)
There are a couple of things to see here:
- It shows the current file that was started (
breakpointing.py) - After that, it shows the line where the program stopped: the call to
breakpoint()itself. Theprint(a)on the next line hasn’t run yet. (Before Python 3.13, the debugger stopped on the line afterbreakpoint()instead, so older tutorials show-> print(a)here.) - And finally, we see the pdb prompt, waiting for our input.
The Python debugger is actually a fully functioning Python shell, just like the REPL.
At this point, we can inspect the current environment. Let’s see what the value of a is right now:
(Pdb) print(a)
3
(Pdb) print(i)
1Code language: Python (python)
Instead of print, you can also use the pdb specific command p, like this:
(Pdb) p a
3
(Pdb) p i
1Code language: Python (python)
Running the next line
So what if we want to run the next line of code? There are two options:
- With
sorstep, you execute the current line. The debugger stops at the first possible occasion after that. - With
nornext, you execute the current line as well but now the debugger keeps going until the next line of the current function is reached.
To make this more clear: step happily enters a function call and break inside that function. In contrast, next executes the function until it returns, and breaks on the next line.
Other debuggers and IDEs also call this ‘step into’ (instead of step) and ‘step over’ (instead of next).
Now let’s use these two:
$ python3 breakpointing.py
> /home/erik/breakpointing.py(6)<module>()
-> breakpoint()
(Pdb) n
> /home/erik/breakpointing.py(7)<module>()
-> print(a)
(Pdb) n
3
> /home/erik/breakpointing.py(4)<module>()
-> for i in range(1, 10):
(Pdb) s
> /home/erik/breakpointing.py(5)<module>()
-> a = add_two(i)
(Pdb) n
> /home/erik/breakpointing.py(6)<module>()
-> breakpoint()
(Pdb) n
> /home/erik/breakpointing.py(6)<module>()
-> breakpoint()
(Pdb) n
> /home/erik/breakpointing.py(7)<module>()
-> print(a)
(Pdb) s
4
> /home/erik/breakpointing.py(4)<module>()
-> for i in range(1, 10):
(Pdb) s
> /home/erik/breakpointing.py(5)<module>()
-> a = add_two(i)
(Pdb) s
--Call--
> /home/erik/breakpointing.py(1)add_two()
-> def add_two(x):
(Pdb) s
> /home/erik/breakpointing.py(2)add_two()
-> return x + 2
(Pdb) s
--Return--
> /home/erik/breakpointing.py(2)add_two()->5
-> return x + 2
(Pdb) s
> /home/erik/breakpointing.py(6)<module>()
-> breakpoint()
(Pdb)Code language: Python (python)
As you can see, we first step over the call to add_two with n. The second time, we step into the function with s. When the function returns, pdb shows the return value: ->5.
You may notice that we stop on the breakpoint() line twice in a row. The first stop is the debugger arriving at that line. Then n runs the line, which calls breakpoint() again and starts a fresh stop on the same line.
Python debugger commands
There are more commands that you can try on your own. I won’t list them all, just the most useful ones:
h(help): print all available commands. Also takes an argument, e.g.h ctells you what theccommand doesc(continue): start running until the next breakpointl(list): list the source code around the current linep: print the result of the following expression. You can use this instead of callingprint()pp: pretty print, useful for printing data structures like dictionariesb(break): set a breakpoint, e.g.b 7for line 7 orb add_twofor a function. Without an argument, it lists all breakpointsw(where): show the stack trace, so you can see which function called whichu(up) andd(down): move up and down the stack, so you can inspect the variables of the function that called the current onerestart: restart the current programq(quit): quit the debugger. If you started it withbreakpoint(), pdb asks for confirmation first, because quitting stops your program
For the complete documentation, head over to the Python debugger page, where all the commands are listed.
Other ways to start the Python debugger
You don’t have to edit your code to use pdb. You can also start your script through the debugger with python3 -m pdb. It stops at the first line, so you can set breakpoints before anything runs. Let’s try it on this little script, crash.py, which has a bug:
def average(numbers):
return sum(numbers) / len(numbers)
scores = []
print(average(scores))Code language: Python (python)
$ python3 -m pdb crash.py
> /home/erik/crash.py(1)<module>()
-> def average(numbers):
(Pdb) b average
Breakpoint 1 at /home/erik/crash.py:2
(Pdb) c
> /home/erik/crash.py(2)average()
-> return sum(numbers) / len(numbers)
(Pdb) p numbers
[]Code language: Python (python)
Post-mortem debugging
Often, you only want the debugger when something goes wrong. Add -c continue, and pdb runs your script normally. When it crashes, you get the traceback, and then you land in the debugger at the exact spot of the error, with all variables still intact. This is called post-mortem debugging:
$ python3 -m pdb -c continue crash.py
...
ZeroDivisionError: division by zero
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /home/erik/crash.py(2)average()
-> return sum(numbers) / len(numbers)
(Pdb) p numbers
[]
(Pdb) u
> /home/erik/crash.py(5)<module>()
-> print(average(scores))
(Pdb) p scores
[]Code language: Python (python)
I left out the traceback here to keep it short. Note how u takes us one level up, to the code that called average(), so we can see where the empty list came from.
The same works in the REPL. When a piece of code raised an exception, type import pdb and then pdb.pm() to inspect the moment of the crash.
Attaching to a running program
Since Python 3.14, you can even attach pdb to a Python program that’s already running, using its process ID: python3 -m pdb -p 1234. That’s useful for a program that seems stuck. Depending on your operating system, you may need extra permissions to attach to another process. The pdb documentation has the details.
Keep learning
Although this can come in super handy, it pays to dive into the debugger that your IDE offers. If you’re a VSCode user, I wrote a tutorial on how to debug Python code in VSCode.
