Nobody likes to spend time on debugging when working under a tight deadline. But we all want our applications to work, so debugging code is a must in every project. Developers often turn to debuggers in such situations, and Python offers a solution like that.
In this article, I take a closer look at Python's interactive source code debugger called pdb to show you its value for debugging.
Debuggers let us see the state of any variable in an application, allowing us to stop and resume the application's execution process anytime to see how every line of code impacts its internal state. Looking through code with pdb helps to identify bugs that are usually hard to find and fix problematic code quickly.
pdb is included in Python’s standard library. It's a source code debugger you can use in any situation, but it's particularly handy if you need to debug code in an environment where you can't access a GUI debugger that you already know.
Ready to see pdb in action? Let's dive in.
Before Python 3.7, we could initialize the debugger using pdb.set_trace(). But there's a new way available now: the built-in function breakpoint() helps to enter the debugger easily. By default, breakpoint() imports pdb and calls pdb.set_trace(). It's smart to use this method because it's more flexible and gives you complete control over debugging behavior through its API and the environment variable PYTHONBREAKPOINT.
By using the print command p, we'll be passing an expression to be evaluated by Python. If we pass a variable name, pdb will print its current value. However, there's much more we can do to examine the state of our running application.
To step through code when debugging, you can use two commands:
n (next) – continue execution until the next line in the current function is reached or it returns.
s (step) – execute the current line and stop at the first possible occasion (in a function which is called or the current function).
As you can see, the difference between n (next) and s (step) is where we set pdb to stop. n (next) is all about staying local as it stays within the current function. s (step), on the other hand, allows us to stop in a foreign function if we call one.
When the end of the current function is reached, both n (next) and s (step) will print --Return-- together with the return value at the end of the next line.
The command ll (longlist) will list the entire source code for the current function or frame. It comes in handy when we're going through code we don't know or want to see the whole function for more context.
To see a shorter snippet of source code, we can use the l (list) command, which will print 11 lines around the current line or go on to the previous listing.
Breakpoints save a lot of time when debugging – instead of stepping through many lines that aren't of interest, we can create a breakpoint exactly where we want to investigate.
To set a breakpoint, we use the b (break) command . We can specify a code line number or a function name where execution is to be stopped following this syntax: b(reak) [ ([filename:]lineno | function) [, condition] ]. We can disable and re-enable breakpoints using the disable bpnumber and enable bpnumber commands (where bpnumber is the breakpoint number from the first column of the breakpoints list).
Another option we can use here is the c (continue) command, which continues execution until a breakpoint is found. We can also set a temporary breakpoint with the help of the tbreak command, which uses the same arguments as b.
We use the display [expression] command to have pdb automatically display the value of an expression when execution stops in the current frame (if that expression changed). The frame is just a data structure that is created when a function is called, and deleted after returning from that function. The opposite of that is the undisplay [expression] command, which is used to clear a displayed expression in the current frame. If there's no expression, the command will clear all display expressions for the current frame.
It's also possible to enter display multiple times to create a watch list of expressions. All it takes is adding all the expressions we're interested in and entering display to see their current values.
To find the code caller, we can use the w (where) command, which will print a stack trace (list of all the frames, showing the most recent frame at the bottom - this way we can start there and read from the bottom up. We can use two other commands, namely u (up) and d (down) to change the current frame. When we combine it with p, we can inspect variables and their state in our application at any point in any frame. Finally, we can continue execution until the current function returns with the help of the r (return) command.
Users of pdb can take advantage of the handy h (help) command, which brings up relevant knowledge. It's enough to enter h or help <topic> to get a list of all commands or specific topics.
pdb has extensive documentation that offers plenty of useful knowledge – be sure to check it out.
pdb is a handy tool for debugging which all Python developers should know. If you have any questions about using pdb, feel free to leave a comment – we'd be happy to share our knowledge and start a conversation about best debugging practices in Python.