May 17, 2026 - python - debugging

How to Debug Python Applications with pdb: Part 1

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 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.