Learn what Python's built-in debugger is, why it's essential for troubleshooting, and how to step through your code line-by-line using breakpoint() and basic pdb commands.
What is pdb and why is it so powerful?
When a script crashes or produces unexpected results, the most common developer reflex is to scatter print() statements
everywhere and re-run the code hoping to catch the bug. While this works for simple scripts, it quickly becomes a
tedious, messy, and time-consuming process in complex applications.
This is where pdb (the Python Debugger) comes in. It allows you to pause your program mid-execution, drop into an
interactive shell, and inspect the exact state of your application at that very moment. pdb ships with every Python
installation, so there is nothing to install. You can immediately start using it.
This blog post covers the basics of working with pdb. By just mastering the basics you will be able to cover
most, if not all, of your debugging use cases.
The buggy code:
To help illustrate the concepts, I've written a small (and buggy) function for a bakery that calculates the total of
a product based on the desired amount and applies a 10% discount if more than 4 of that item is ordered. To make sure
our math is correct, I've added a few assert statements in the main() function. This should point out if we have any
issues.
The code
# Product name: Product price
products = {
"croissant": 12,
"muffin": 20,
"scone": 15,
}
def calculate_total(product_name: str, amount: int) -> float:
"""
Calculates the total amount owed for a product. If more than 4 of the product is ordered, a 10% discount is applied
to the total.
Args:
product_name (str): The name of the product
amount (int): The amount of the product ordered
Returns:
The total owed
"""
price = products[product_name]
total = price * amount
if amount >= 5:
total = total - (price * 0.10)
return total
def main():
total = calculate_total("muffin", 0)
assert total == 0.0
total = calculate_total("muffin", 2)
assert total == 40.0
# More than 4 items ordered, a 10% discount should be applied
total = calculate_total("muffin", 5)
assert total == 90.0
if __name__ == "__main__":
main()
At first glance everything looks fine, however, when running the code we can see the bug in action.
Traceback (most recent call last):
File "/home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py", line 42, in <module>
main()
~~~~^^
File "/home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py", line 38, in main
assert total == 90.0
^^^^^^^^^^^^^
AssertionError
It seems like one of our totals aren't what we expect it to be. Apart from the total not being equal to 90, we don't
really get much else from the output. For a start, I would want to know what total actually was. Generally you could
just add a print(total) above the line that contains the assert to see the value. This would work fine and you would
know the value of total but apart from that you would not know much else. You then need to go to the
calculate_total function and add a bunch of print() statements in it until you find the place where the issue appears.
Let's instead use pdb by adding a breakpoint above the line that contains the assert and step through the code
line-by-line.
Inserting a breakpoint
A breakpoint can be inserted directly into the source code in one of two ways:
For versions of Python 3.6 or earlier you need to add it like this:
import pdb; pdb.set_trace()
A more modern syntax was introduced for Python version 3.7 and higher. You can simply call the globally available
breakpoint() function. No need for any imports.
breakpoint()
Note: We'll be using the more modern breakpoint() version
So let's add our breakpoint above the failing assert statement:
# More than 4 items ordered, a 10% discount should be applied
total = calculate_total("muffin", 5)
breakpoint()
assert total == 90.0
Running the code now allows for something interesting to happen. The execution of the program halts on the line where we set our breakpoint, dropping us into the interactive debugger prompt / shell.
The output should look something like this:
> /home/karel/code/scripts/bakery.py(39)main()
-> breakpoint()
Let's dissect the output before we continue:
Line 1: (> /home/karel/code/scripts/bakery.py(39)main()) tells you what Stack Frame you are currently looking at.
The concept of a Stack Frame will be covered in Part 2 of working with pdb. For now just know that it just gives you
more information about the current code being executed. In this case, the script we are running (bakery.py), the line
number we are on (39), and the current function being executed (main).
Line 2: (-> breakpoint()) tells you about the following line that will be executed. Note that it does not show
you the line that was executed, but rather the one about to be executed.
Important: In Python version 3.11 the behaviour of hitting a breakpoint changed a little due to
PEP 657. PEP 657 introduced an overhaul of how execution is tracked. As a
result, later versions of Python halts the instant it hits the breakpoint function and it appears as though the
function was not invoked yet. In reality this is not the case. The breakpoint function did in fact run but the
shell does not move to the next line until you explicitly move it to the next line (using the n command). So if you
are using an older version of python your prompt will show that the next line to be executed is the line that contains
the assert: -> assert total == 90.0
The basics debugger commands
This is where the real usefulness and power of pdb comes in. Once you are in the interactive debugger shell you can do
all kinds of wonderful things, like looking at the values of variables, execute your own python code (written in-line),
step into and out of functions, and much more. We'll start with the basic commands to get us up and running so that
we can find the bug and squash it.
Commands are entered directly into the shell and executed by pressing the enter key.
Finding help
It's handy to know where to find more information on the commands available. You can do so with the h command.
Simply enter h and press the enter key. You should see all the available command printed to the screen:
(Pdb) h
Documented commands (type help <topic>):
========================================
EOF cl disable ignore n return u where
a clear display interact next retval unalias
alias commands down j p run undisplay
args condition enable jump pp rv unt
b cont exceptions l q s until
break continue exit list quit source up
bt d h ll r step w
c debug help longlist restart tbreak whatis
Miscellaneous help topics:
==========================
exec pdb
You can also get information on a specific command by entering the command after the h. Let's look at the list
command by entering h list.
(Pdb) h list
Usage: l(ist) [first[, last] | .]
List source code for the current file. Without arguments,
list 11 lines around the current line or continue the previous
listing. With . as argument, list 11 lines around the current
line. With one argument, list 11 lines starting at that line.
With two arguments, list the given range; if the second
argument is less than the first, it is a count.
The current line in the current frame is indicated by "->".
If an exception is being debugged, the line where the
exception was originally raised or propagated is indicated by
">>", if it differs from the current line.
As you can see, it prints a detailed description of what the list command does as well as some helpful aliases that
exist for it. In the case of list, the l alias can be used instead of typing list .. You can have a look at the
other commands in the same way.
Control commands
The two main control commands we will highlight in this part of understanding pdb are the quit and continue
commands.
If we have a look at the help output for the quit command we can see that it simply quits the debugger. Something to
note is that using the quit command aborts the execution of your application so you will need to restart it if you want
it to debug again or have the application run.
(Pdb) h quit
Usage: q(uit) | exit
Quit from the debugger. The program being executed is aborted.
Note: Instead of typing quit or exit, you can simply just run q.
On the other hand, continue tells the debugger to continue the execution of your application until it finishes or it
hits another breakpoint (yes, you can have as many breakpoints as you want). I don't find myself using q much unless
I want to do something like prevent an HTTP request from being fired off. Here is a look at the help output of
continue:
(Pdb) h continue
Usage: c(ont(inue))
Continue execution, only stop when a breakpoint is encountered.
Note: Instead of typing continue, you can simply run c or cont.
Context commands
Now that we know how to abort and continue. Let's see how we can find more information on the context our current line
is about to be executed in. For that we can reach for the various list commands. The two main ones are list and ll
and are used to print the lines of source code above and below the line to be executed.
The list command takes several arguments. The first is a fullstop (.) which in turn prints 11 lines around the
current line to be executed. This looks like this: list .; also aliased to just l. If any other number is specified
after list, it print 11 lines starting at the specified line number. This could look like this: list 1. It prints
the first 11 lines of code in the current file.
This is what the help output says about the list command:
(Pdb) h list
Usage: l(ist) [first[, last] | .]
List source code for the current file. Without arguments,
list 11 lines around the current line or continue the previous
listing. With . as argument, list 11 lines around the current
line. With one argument, list 11 lines starting at that line.
With two arguments, list the given range; if the second
argument is less than the first, it is a count.
The current line in the current frame is indicated by "->".
If an exception is being debugged, the line where the
exception was originally raised or propagated is indicated by
">>", if it differs from the current line.
The ll (or longlist) command prints all the lines in the current function or the Stack Frame. This is useful after
stepping into a function and allows you to quickly read the code in that function before you continue stepping through
the rest of the lines.
This is what the help output says about the ll command:
(Pdb) h ll
Usage: ll | longlist
List the whole source code for the current function or frame.
With these two commands you can easily see where you are, what's coming up, and what's happened already.
Inspection commands
The inspection commands are used to look at the values of variables. The two main ones are p and pp. You can also
just type the variable name to have it's value printed in the shell. For example you can type total and its value
will be printed.
(Pdb) total
98.0
The same happens when you type p total:
(Pdb) p total
98.0
However, the p command is not just used to print the value of a variable. You can also give it an expression and it
will evaluate it and print the result. For example, you could do something like p total * 2 / 5. I have found it to
be very useful in the past when having to decipher complicated and and or logic in if statements.
(Pdb) p total * 2 / 5
39.2
This is what the help output says about the p command:
(Pdb) h p
Usage: p expression
Print the value of the expression.
The pp command essentially does the same as the p command with the exception of pretty printing the value of big
dictionaries or lists. If the output of p is hard to read, give pp a try.
This is what the help output says about the pp command:
(Pdb) h pp
Usage: pp expression
Pretty-print the value of the expression.
Navigation commands
Navigation commands are the core engine of the debugger. They control how execution moves through your code, allowing you to step through it line-by-line, dive into functions, or skip over them entirely. These are the commands you will find yourself using the most.
The n (next) command executes the current line of code and pauses at the very next line in the current function.
The key thing to understand about n is that it steps over function calls. If the current line contains a call to
another function, n will run that entire function behind the scenes and land you on the line after the call.
You don't go into the function.
The s (step) command also executes the current line, but with one important difference. If that line contains a
function call, it steps into that function so you can debug it line-by-line. This is how you follow execution
deeper into your code. If there is no function call on the current line, it behaves exactly like n.
Just be careful with this one as you could very easily end up stepping into some library function and get completely lost. How to recover from that will be covered in the next part.
Once you've stepped into a function you might decide you don't need to walk through every line of it. The r
(return) command continues running the code until the current function is about to hit its return statement. This is
the fastest way to "step out" of a function if you stepped into it by accident and want to get back to where you were.
I'll show these in the next section where we will navigate through and fix our bug.
Fixing the bug with pdb
Important: Don't worry about making a spelling mistake while typing a variable name or referencing a variable that does not have a value yet. You will just get a message like this: (Pdb) price *** NameError: name 'price' is not defined
Now that we know the basic command, let's get on with fixing our bug. We'll start by running the code and having a look
out what we can find out about with the debugger.
> /home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py(39)main()
-> breakpoint()
(Pdb) n
> /home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py(40)main()
-> assert total == 90.0
Here we are. I've moved the next line to be executed past the breakpoint line. Now we can see that the line
containing the assertion will be run next. We know that it is where our tests fail. Let's print total using the p
command to see what its value is.
(Pdb) p total
98.0
We can see that our total is not 90.0 as we expect. We'll let our debugger continue executing using the c command.
I think we have learned all we can from the current location of our breakpoint.
(Pdb) c
Traceback (most recent call last):
File "/home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py", line 44, in <module>
main()
~~~~^^
File "/home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py", line 40, in main
assert total == 90.0
^^^^^^^^^^^^^
AssertionError
We have a few options. We know that the error is coming from the calculate_total function. We can add a breakpoint
in the function and hit c after hitting the first breakpoint. It will continue until we hit the second breakpoint. I
will, however, opt for moving the breakpoint one line higher in the source code so that we break before reaching the
function call. This will allow us to step into the function and resume our search there.
This is a full overview of the current code:
# Product name: Product price
products = {
"croissant": 12,
"muffin": 20,
"scone": 15,
}
def calculate_total(product_name: str, amount: int) -> float:
"""
Calculates the total amount owed for a product. If more than 4 of the product is ordered, a 10% discount is applied
to the total.
Args:
product_name (str): The name of the product
amount (int): The amount of the product ordered
Returns:
The total owed
"""
price = products[product_name]
total = price * amount
if amount >= 5:
total = total - (price * 0.10)
return total
def main():
total = calculate_total("muffin", 0)
assert total == 0.0
total = calculate_total("muffin", 2)
assert total == 40.0
# More than 4 items ordered, a 10% discount should be applied
breakpoint()
total = calculate_total("muffin", 5)
assert total == 90.0
if __name__ == "__main__":
main()
Let's run it again and step through it one action at a time.
> /home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py(38)main()
-> breakpoint()
(Pdb) n
> /home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py(39)main()
-> total = calculate_total("muffin", 5)
(Pdb)
Here we can see that the next line to be executed is the line with the function call. We can step into it using the s
command.
Important: If you just want the value of the function call you can use the p command to run it without saving its value:
(Pdb) p calculate_total("muffin", 5)
98.0
Let's step into it instead.
(Pdb) s
--Call--
> /home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py(9)calculate_total()
-> def calculate_total(product_name: str, amount: int) -> float:
Here we can see that we are now about to enter the function. Let's advance it further using the n command.
> /home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py(21)calculate_total()
-> price = products[product_name]
This is a bit jaring. We are now looking at completely different code in a completely different Stack Frame. We can
use the ll command to regain our barings.
(Pdb) ll
9 def calculate_total(product_name: str, amount: int) -> float:
10 """
11 Calculates the total amount owed for a product. If more than 4 of the product is ordered, a 10% discount is applied
12 to the total.
13
14 Args:
15 product_name (str): The name of the product
16 amount (int): The amount of the product ordered
17
18 Returns:
19 The total owed
20 """
21 -> price = products[product_name]
22 total = price * amount
23
24 if amount >= 5:
25 total = total - (price * 0.10)
26
27 return total
The ll command printed the code of the entire function. This is a nice quick way to see what we are dealing with. At
this stage I like to do some extra inspecting by looking at the values of the arguments as well as the global variables,
like our products dictionary.
(Pdb) p product_name
'muffin'
(Pdb) p amount
5
(Pdb) p products
{'croissant': 12, 'muffin': 20, 'scone': 15}
Note: You could also just have printed the variables by referencing their names: (Pdb) product_name.
We can see that out product is a "muffin" and that someone is ordering 5 of them. Let's advance a bit further. My
theory is that the issue comes in at our if statement where we calculate the new total. I'll continue on a bit.
> /home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py(21)calculate_total()
-> price = products[product_name]
(Pdb) n
> /home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py(22)calculate_total()
-> total = price * amount
(Pdb) n
> /home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py(24)calculate_total()
-> if amount >= 5:
(Pdb) price
20
(Pdb) total
100
I've advanced us to the point where we are about to enter the if statement. I've also printed the price extracted
from the dictionary and the initial total that was calculated. We can take a sneak peak to see if our code path will
lead us into the if statement body like this:
(Pdb) amount >= 5
True
We can see that we will continue into the if statement body because the expression equates to True. We continue
advancing to the next line where we are calculating the new total.
> /home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py(25)calculate_total()
-> total = total - (price * 0.10)
Here we are. The suspected source of our bug. Let's start by evaluating the expression to see what the result of our calculation would be.
(Pdb) total - (price * 0.10)
98.0
Bingo! It looks like we are calculating the 10% discount on a single item instead of on the full total. We should replace the single price with the total inside the round brackets. Additionally we know that our expected number is 90.
We can test our theory by evaluating the expression that we believe is correct in the shell.
(Pdb) total - (total * 0.10)
90.0
That looks better. We have found our bug but before we fix it I want to show one other thing that should be kept in
mind when working with pdb. Any changes we make to variables are persistent. To show this I'm going to advance to the
point just before we return the total. By then our total will be set to the wrong value and I will then
manually update the total to be the expected 90 and we should see that our tests pass.
> /home/karel/code/karelschwab_com_v2/markdown_files/posts/scripts/bakery.py(28)calculate_total()
-> return total
(Pdb) total
98.0
(Pdb) total = 90
(Pdb) total
90
(Pdb) c
Important: Keep in mind that any changes made to variables after they have been set persist for the remainder of the program execution.
The AssertionError is not raised because the value is the 90 that I set. If you remove the breakpoint and run the
code again the error appears again. Now that we know the cause of our calculation error. We can fix the bug.
The final version of the code:
# Product name: Product price
products = {
"croissant": 12,
"muffin": 20,
"scone": 15,
}
def calculate_total(product_name: str, amount: int) -> float:
"""
Calculates the total amount owed for a product. If more than 4 of the product is ordered, a 10% discount is applied
to the total.
Args:
product_name (str): The name of the product
amount (int): The amount of the product ordered
Returns:
The total owed
"""
price = products[product_name]
total = price * amount
if amount >= 5:
total = total - (total * 0.10)
return total
def main():
total = calculate_total("muffin", 0)
assert total == 0.0
total = calculate_total("muffin", 2)
assert total == 40.0
# More than 4 items ordered, a 10% discount should be applied
total = calculate_total("muffin", 5)
assert total == 90.0
if __name__ == "__main__":
main()
And there we have it. With the basics of pdb you will be able to squash most bugs that you come across. In part 2
of exploring pdb we will cover the advanced concepts like post mortem debugging, traveling up and
down the call stack / stack frame (also known as time traveling), aliasing, and advanced movements like with
the unt command.
Also make sure to check out the official documentation site for pdb.