Back to Scaling Python Applications guides

A Guide to Debugging Python Code with ipdb

Stanley Ulili
Updated on March 5, 2025

Debugging is an important part of software development, and having the right tools can significantly improve efficiency and accuracy in identifying issues.

ipdb is a debugging tool for Python that extends the functionality of the built-in Python debugger (pdb).

It integrates features from IPython, such as syntax highlighting, tab completion, and an enhanced command set, making debugging more interactive and user-friendly.

With ipdb, you can inspect variables, set breakpoints, and step through code more effectively.

This article'll explore how to leverage ipdb to debug Python applications efficiently.

Prerequisites

Before getting started, ensure you have:

  • The latest version of Python (3.13+) installed on your system.
  • A basic understanding of building applications with Python.

Setting up the project directory

To keep things organized, you’ll set up a dedicated project directory and a virtual environment for your debugging examples.

Create a new directory for our debugging project and move into it:

 
mkdir python_debugging && cd python_debugging

Create a virtual environment:

 
python3 -m venv venv

Activate the virtual environment to ensure that any installed packages are contained within this project:

 
source venv/bin/activate

Now that you have your virtual environment activated, install ipdb:

 
pip install ipdb

This isolates your debugging dependencies from your system Python installation and makes package management cleaner.

Debugging Python using ipdb from the command-line

The ipdb package enhances Python's built-in debugger (pdb) with features from IPython, including tab completion, syntax highlighting, and better tracebacks. It's particularly useful when you need to debug scripts directly from the command line.

Let's create a simple Python script that you'll use for our debugging examples. Save the following code as fibonacci.py in your project directory:

fibonacci.py
def fibonacci(n):
    """Generate Fibonacci sequence up to n"""
    fib_sequence = [0, 1]

    while fib_sequence[-1] + fib_sequence[-2] <= n:
        next_fib = fib_sequence[-1] + fib_sequence[-2]
        fib_sequence.append(next_fib)

    return fib_sequence

print(fibonacci(100))

When you run this script normally, it will calculate and print the Fibonacci sequence up to 100:

 
python fibonacci.py
Output
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

To debug this script using ipdb, you can invoke Python with the -m ipdb flag followed by your script name:

 
python -m ipdb fibonacci.py
Output
> /path/to/fibonacci.py(1)<module>()
----> 1 def fibonacci(n):
      2     """Generate Fibonacci sequence up to n"""
      3     fib_sequence = [0, 1]

ipdb>

When you launch ipdb this way, several things happen:

  1. The debugger starts before any code executes
  2. Execution pauses at the first line of your script
  3. The (Pdb+) prompt appears, indicating that ipdb is ready to accept commands
  4. The current line is displayed with an arrow (->) showing where execution will continue

From this point, you can control the execution flow using various debugger commands:

  • n or next: Execute the current line and move to the next line in the same function
  • s or step: Step into a function call
  • c or continue: Continue execution until the next breakpoint or end of program
  • q or quit: Exit the debugger
  • l or list: Show the code context around the current line
  • p <expression>: Print the value of an expression
  • pp <expression>: Pretty print the value (useful for complex data structures)
  • h or help: Display help about available commands

These commands form the foundation of your debugging toolkit. Let's explore how to use them effectively by setting a breakpoint inside the fibonacci function using the b or break command:

 
(Pdb+) b 5
Output
Breakpoint 1 at /path/to/fibonacci.py:5

Now you can continue execution until this breakpoint is hit:

 
(Pdb+) c
Output
> path/to/fibonacci.py(5)fibonacci()
      4 
1---> 5     while fib_sequence[-1] + fib_sequence[-2] <= n:
      6         next_fib = fib_sequence[-1] + fib_sequence[-2]

At this point, you can inspect variables using the p command:

 
(Pdb+) p fib_sequence
Output
[0, 1]
 
(Pdb+) p n
Output
100

One of ipdb's strengths is the ability to enter IPython-style commands directly. For instance, you can use tab completion to explore available attributes or use ? to get help on objects:

 
(Pdb+) fib_sequence.

Pressing tab here will show available methods on the list.

 
(Pdb+) fib_sequence?

Screenshot showingavailable methods on the list

This will display documentation about the list object.

To see all available local variables, you can use:

 
(Pdb+) locals()
Output
{'n': 100, 'fib_sequence': [0, 1]}

To execute the next iteration of the loop, type n:

 
(Pdb+) n
Output
> /path/to/fibonacci.py(6)fibonacci()
1     5     while fib_sequence[-1] + fib_sequence[-2] <= n:
----> 6         next_fib = fib_sequence[-1] + fib_sequence[-2]
      7         fib_sequence.append(next_fib)

You can then execute another step:

 
(Pdb+) n
Output
> /path/to/fibonacci.py(7)fibonacci()
      6         next_fib = fib_sequence[-1] + fib_sequence[-2]
----> 7         fib_sequence.append(next_fib)
      8 

And check the value of next_fib:

 
(Pdb+) p next_fib
Output
1

If you want to set a condition for your breakpoint to trigger only in specific situations, you can use a conditional breakpoint:

 
(Pdb+) b 5, next_fib > 50
Output
Breakpoint 2 at /path/to/fibonacci.py:5

To see all active breakpoints, use:

 
(Pdb+) b
Output
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /path/to/fibonacci.py:5
2   breakpoint   keep yes   at /path/to/fibonacci.py:5
        stop only if next_fib > 50

Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /path/to/fibonacci.py:5

        breakpoint already hit 1 time
2   breakpoint   keep yes   at /path/to/fibonacci.py:5
        stop only if next_fib > 50

You can remove a breakpoint using the clear command followed by the breakpoint number:

 
(Pdb+) clear 1
Output
Deleted breakpoint 1 at path/to/fibonacci.py

Now that you can use ipdb to debug Python scripts from the command line, you will set up breakpoints in your code next.

Using ipdb directly in your code

Instead of launching the debugger from the command line, you can insert breakpoints directly in your code using the set_trace() function. This approach is particularly useful when:

  • You already know where the problem might be
  • You only want to examine a specific section of code
  • You need to debug code that's called from another script
  • You're working with a more complex application architecture

Let's see how to integrate ipdb directly into your Python code:

fibonacci_with_breakpoint.py
import ipdb

def fibonacci(n):
    """Generate Fibonacci sequence up to n"""
    fib_sequence = [0, 1]

    ipdb.set_trace()  # Execution will pause here when running the script

    while fib_sequence[-1] + fib_sequence[-2] <= n:
        next_fib = fib_sequence[-1] + fib_sequence[-2]
        fib_sequence.append(next_fib)

    return fib_sequence

print(fibonacci(100))

When you run this script normally:

 
python fibonacci_with_breakpoint.py

The program execution will pause at the set_trace() line, and you'll be dropped into the ipdb prompt:

Output
> /path/to/fibonacci_with_breakpoint.py(9)fibonacci()
      7 
----> 8     ipdb.set_trace()  # Execution will pause here when running the script
      9 
ipdb>

Notice a few differences compared to launching from the command line:

  1. The arrow ----> shows the exact line that will execute next (this IPython-style formatting is more visually informative than standard pdb)
  2. You see line numbers and surrounding code context automatically
  3. The debugger prompt is labeled ipdb> rather than (Pdb+) (though functionally they're identical)

Using the built-in breakpoint() function

Since Python 3.7, there's an even simpler way to add debugging breakpoints to your code using the built-in breakpoint() function:

 
# remove import ipdb
def fibonacci(n): """Generate Fibonacci sequence up to n""" fib_sequence = [0, 1]
breakpoint() # This will use ipdb if installed, or pdb by default
while fib_sequence[-1] + fib_sequence[-2] <= n: # ...code continues...

This approach has several advantages:

  • No imports required: You don't need to explicitly import ipdb
  • Environment-aware: Uses whatever debugger is configured in your environment
  • Easier to find: When scanning code, breakpoint() stands out as an intentional debugging statement
  • Quick to disable: You can disable all breakpoints by setting the PYTHONBREAKPOINT=0 environment variable

When running the script with breakpoint(), the execution pauses at the specified line, allowing you to inspect the current state of variables and interact with the debugger:

 
python fibonacci_with_breakpoint.py
Output
> path/to/fibonacci_with_breakpoint.py(5)fibonacci()
-> breakpoint()

You can also disable all breakpoint() calls:

 
PYTHONBREAKPOINT=0 python fibonacci_with_breakpoint.py
Output
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

This flexibility makes breakpoint() the preferred method for adding debugging points in modern Python code.

Handling exceptions with post-mortem debugging

Another handy feature of ipdb is post-mortem debugging, enabling you to analyze errors after an exception. This is especially useful for diagnosing issues in production code or investigating unexpected failures.

Let's create a deliberately buggy script to demonstrate this technique:

buggy_fibonacci.py
def buggy_fibonacci(n):
    """A buggy Fibonacci implementation"""
    fib_sequence = []  # Bug: empty list instead of [0, 1]!

    while fib_sequence[-1] + fib_sequence[-2] <= n:  # Will raise IndexError
        next_fib = fib_sequence[-1] + fib_sequence[-2]
        fib_sequence.append(next_fib)

    return fib_sequence

print(buggy_fibonacci(100))

When you run this script, you'll get an error due to the empty list:

 
python buggy_fibonacci.py
Output
  File "/path/to/python_debugging/buggy_fibonacci.py", line 12, in <module>
    print(buggy_fibonacci(100))
          ~~~~~~~~~~~~~~~^^^^^
  File "/path/to/python_debugging/buggy_fibonacci.py", line 5, in buggy_fibonacci
    while fib_sequence[-1] + fib_sequence[-2] <= n:  # Will raise IndexError
          ~~~~~~~~~~~~^^^^
IndexError: list index out of range

Instead of modifying the code and running it repeatedly, you can examine the state at the point of failure using ipdb's post-mortem capability:

 
python -m ipdb buggy_fibonacci.py
 
> /path/to/python_debugging/buggy_fibonacci.py(1)<module>()
----> 1 def buggy_fibonacci(n):
      2     """A buggy Fibonacci implementation"""
      3     fib_sequence = []  # Bug: empty list instead of [0, 1]!
ipdb> c

After pressing c to continue execution, the error occurs and ipdb automatically enters post-mortem mode, placing you at the exact point where the error happened:

 
Traceback (most recent call last):
...
IndexError: list index out of range
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /path/to/python_debugging/buggy_fibonacci.py(5)buggy_fibonacci()
      4 
----> 5     while fib_sequence[-1] + fib_sequence[-2] <= n:  # Will raise IndexError
      6         next_fib = fib_sequence[-1] + fib_sequence[-2]
ipdb>

You can examine variables, step through code, and determine what caused the error from this point. In this case, you'd discover that fib_sequence is an empty list that needs proper initialization with the first two Fibonacci numbers [0, 1] before attempting to access elements.

The power of post-mortem debugging is that we're now positioned exactly at the point of failure, with all variables in the state they were in when the error occurred. you can inspect the program's state to understand what went wrong:

 
ipdb> p fib_sequence
Output
[]
 
ipdb> p n
Output
100

The problem is now apparent: fib_sequence is an empty list, but our algorithm is trying to access elements that don't exist. This insight would allow you to fix the list initialization in a real debugging scenario. .

Creating command aliases

When debugging with ipdb, you may repeatedly run the same commands or evaluate similar expressions. To simplify your workflow, ipdb allows you to create command aliases, which act as shortcuts for frequently used commands.

Aliases help reduce typing effort and improve efficiency, especially when dealing with complex expressions or multi-step debugging processes. You can define an alias using the alias command followed by the shortcut name and the command you want it to execute.

For example, if you frequently check the length of fib_sequence, you can create an alias like this:

 
ipdb> alias pl p len(fib_sequence)  # Create an alias for printing sequence length
ipdb> pl  # Use the alias
Output
0

Using aliases can make debugging smoother by reducing repetitive typing and improving efficiency.

Final thoughts

This article explored how ipdb enhances Python debugging with a more interactive and efficient approach. You can simplify issue diagnosis by setting breakpoints, stepping through code, and using post-mortem debugging.

For more features and best practices, visit the official ipdb documentation.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
Flask vs FastAPI: An In-Depth Framework Comparison
This guide compares Flask and FastAPI, two leading Python web frameworks. Flask offers simplicity and flexibility, while FastAPI provides high performance, async support, and automatic documentation. Explore their architecture, performance, and best use cases to find the right fit for your project.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github