Mastering Python Debugger (pdb): A Comprehensive Guide

Introduction to Python's pdb Debugger

In this short tutorial, we will discuss and see examples of using pdb, Python's built-in debugger, to help you debug your Python applications efficiently. This guide includes detailed examples and insights into pdb commands and their practical applications.

The Python debugger, or pdb, is an essential tool for developers looking to diagnose and understand issues within their Python code. pdb allows you to step through the code line by line, inspect variable values, evaluate expressions, and modify internal states of the program. It's an invaluable tool for debugging errors, optimizing performance, and learning about Python execution flow, making it a staple in the professional Python developer's toolkit.

What is pdb?

The Python debugger (pdb) provides an interactive debugging environment for Python programs. It allows you to execute programs step by step, inspect variable values, and understand program execution flow, which is crucial for diagnosing and solving complex bugs.

Getting Started with pdb

To use pdb, you can start by inserting a breakpoint in your code, which tells Python to pause execution at that point and launch the debugger.

If using Python 3.7 and above, you add the function breakpoint() at the line where you want the script to pause.
1 print("Hello World!")
2 breakpoint()
3 print("Thank you!")
Before Python 3.7 you will import pdb and use the set_trace() function.
1 print("Hello World!")
2 import pdb; pdb.set_trace()
3 print("Thank you!")

The execution will stop where you put the breakpoint in your code. Then you can interact with the code to get information like variable values and test functions, and more.

Key pdb Commands

Before diving into our example scripts, let's review the essential commands in pdb that you will use to control execution and inspect your programs:

  • breakpoint() - Inserts a breakpoint in the script where the debugger will pause execution.
  • print (p) - Prints the value of a variable or expression.
  • pretty print (pp) - Prints the value of a variable or expression in a well-formatted and more readable manner.
  • continue (c) - Continues execution until the next breakpoint is encountered.
  • list (l) - Displays lines of code around the current line being executed.
  • next (n) - Executes the next line of code without stepping into any functions (step over).
  • step (s) - Steps into the function call at the current line (step into).
  • args (a) - Displays the argument list of the current function call, showing the names and values of all arguments passed to the function.
  • quit (q) - Quits the debugger and ends the program.

You can get a complete list of Debbugger Commands From Official Site Here.

Step By Step Example

Our example consists of two Python files: main.py and helpers.py. These scripts simulate a common scenario where data is gathered, processed, and results are displayed. This example will help illustrate how pdb can be used in a realistic debugging situation.

helper.py
1  
2  
3  def gather_data():
4      return {'name': 'Jane Doe', 'age': 28, 'city': 'New York', 
               'country': 'USA', 'enrolled': True}
5  
6  def process_data(data):
7      data['processed'] = True
8      return data
main.py
1  from helpers import gather_data, process_data
2  
3  def main():
4      data = gather_data()
5      breakpoint()  # Start debugging here
6      processed_data = process_data(data)
7      print("Original Data:", data)
8      print("Processed Data:", processed_data)
9  
10 if __name__ == "__main__":
11     main()

When the main.py script is run, it will pause on line #5. The prompt will show (Pdb). The screen would look like this:

admas@/.../python-debugger-pdb : python3 main.py
> /Users/admas/Demos/python-debugger-pdb/main.py(6)main()
-> processed_data = process_data(data)
(Pdb) 

Using the "p" & "pp" Commands

The most common and most useful use case in debugging is to print the value of a variable. In our example, the variable 'data' is defined on line #4 and the breakpoint is on line #5, so we can easily print the value of 'data'. You can use print(), the command "p" for printing, or "pp" for pretty printing. My go-to is "pp" because it prints in a 'pretty' format, which stands for 'pretty print'. This command is particularly useful when dealing with complex data structures as it makes the output more readable and easier to understand.

Here is what it looks like using print() or p:

admas@/.../python-debugger-pdb : python3 main.py
> /Users/admas/Demos/python-debugger-pdb/main.py(6)main()
-> processed_data = process_data(data)
(Pdb) print(data)
{'name': 'Jane Doe', 'age': 28, 'city': 'New York', 'country': 'USA', 'enrolled': True}
(Pdb) p data
{'name': 'Jane Doe', 'age': 28, 'city': 'New York', 'country': 'USA', 'enrolled': True}
(Pdb)  

And here is what it looks like when we use pp (pretty print)

admas@/.../python-debugger-pdb : python3 main.py
> /Users/admas/Demos/python-debugger-pdb/main.py(6)main()
-> processed_data = process_data(data)
(Pdb) pp data
{'age': 28,
    'city': 'New York',
    'country': 'USA',
    'enrolled': True,
    'name': 'Jane Doe'}
(Pdb) 

The use of pp clearly enhances the readability of the output, especially for nested or complex data structures. It organizes the data in a structured format, which helps in quickly understanding the data's structure and the relationships within it, proving invaluable in complex debugging scenarios.

Using 'pp' allows you to see a structured and formatted view of dictionaries, lists, or any objects which have multiple fields or nested structures, making it easier to debug and understand the flow and state of your application at any point during execution.

Continuing Execution with "c"

When you are done debugging and you want the script to continue to execute, you type 'c' (continue) and press ENTER. This command will 'continue' the script. If there is no other breakpoint, it will run until the end of the script. If there is another breakpoint, then it will run until the next breakpoint is hit. This is useful when you have resolved the issue at the current breakpoint or wish to test the program's behavior up to the next breakpoint.

admas@/.../python-debugger-pdb : python3 main.py
> /Users/admas/Demos/python-debugger-pdb/main.py(6)main()
-> processed_data = process_data(data)
(Pdb) c
Original Data: {'name': 'Jane Doe', 'age': 28, 'city': 'New York', 'country': 'USA', 'enrolled': True, 'processed': True}
Processed Data: {'name': 'Jane Doe', 'age': 28, 'city': 'New York', 'country': 'USA', 'enrolled': True, 'processed': True}
admas@/.../python-debugger-pdb : 

Using the 'c' command effectively allows you to manage the flow of a debugging session, moving from one significant checkpoint to another, or completing the execution once you've finished your checks and fixes.

Using the "l" (list) Command

The next command we will look at is the 'l' (list) command, which will show lines of code around the line being executed. It will look like this:

os/python-debugger-pdb/main.py
> /Users/admas/Demos/python-debugger-pdb/main.py(6)main()
-> processed_data = process_data(data)
(Pdb) l
  1     from helpers import gather_data, process_data
  2  
  3     def main():
  4         data = gather_data()
  5         breakpoint()
  6  ->     processed_data = process_data(data)
  7         print("Original Data:", data)
  8         print("Processed Data:", processed_data)
  9  
 10  
 11     if __name__ == "__main__":
(Pdb) 

The arrow (->) will tell you what the next line that will be executed is. In this case, line #6 will be executed.

Using the "n" (next) Command

The next command you will use is the 'n' (next) command. This executes the next line of code without stepping into any functions (step over). So, it will execute process_data(data) without actually going into the function, and it will pause on the next line, which is #7.

os/python-debugger-pdb/main.py
> /Users/admas/Demos/python-debugger-pdb/main.py(6)main()
-> processed_data = process_data(data)
(Pdb) l
  1     from helpers import gather_data, process_data
  2  
  3     def main():
  4         data = gather_data()
  5         breakpoint()
  6  ->     processed_data = process_data(data)
  7         print("Original Data:", data)
  8         print("Processed Data:", processed_data)
  9  
 10  
 11     if __name__ == "__main__":
(Pdb) n
> /Users/admas/Demos/python-debugger-pdb/main.py(7)main()
-> print("Original Data:", data)
(Pdb) 

If you want to execute one line at a time, keep inputting "n" and then "ENTER". This will execute the next line. It will look like this:

    > /Users/admas/Demos/python-debugger-pdb/main.py(6)main()
    -> processed_data = process_data(data)
    (Pdb) l
      1     from helpers import gather_data, process_data
      2  
      3     def main():
      4         data = gather_data()
      5         breakpoint()
      6  ->     processed_data = process_data(data)
      7         print("Original Data:", data)
      8         print("Processed Data:", processed_data)
      9  
     10  
     11     if __name__ == "__main__":
    (Pdb) n
    > /Users/admas/Demos/python-debugger-pdb/main.py(7)main()
    -> print("Original Data:", data)
    (Pdb) n
    Original Data: {'name': 'Jane Doe', 'age': 28, 'city': 'New York', 'processed': True}
    > /Users/admas/Demos/python-debugger-pdb/main.py(8)main()
    -> print("Processed Data:", processed_data)
    (Pdb) n
    Processed Data: {'name': 'Jane Doe', 'age': 28, 'city': 'New York', 'processed': True}
    --Return--
    > /Users/admas/Demos/python-debugger-pdb/main.py(8)main()->None
    -> print("Processed Data:", processed_data)
    (Pdb) 

Using the "s" (step) Command

Another common command to use is the 's' (step) command. This will step into the function call at the current line (step into). The difference between "n" and "s" is that "n" will just execute the next function, but "s" will step into the next function. This command is particularly useful for diving into function implementations to trace deeper issues or understand the flow within functions better. It will look like this when stepping into a function:

Note that when we put "s" it goes inside the process_data(data) function. But when we did "n" it just executes the process_data(data) function but does not step into it."

The sign > shows 'the filename', 'the function name' and 'the line', that will be executed next.

admas@/.../python-debugger-pdb : python3 main.py
> /Users/admas/Demos/python-debugger-pdb/main.py(6)main()
-> processed_data = process_data(data)
(Pdb) s
--Call--
> /Users/admas/Demos/python-debugger-pdb/helpers.py(9)process_data()
-> def process_data(data):
(Pdb) s
> /Users/admas/Demos/python-debugger-pdb/helpers.py(14)process_data()
-> data['processed'] = True
(Pdb) 

Conclusion

Mastering Python's pdb debugger is crucial for developers aiming to enhance their debugging skills. This comprehensive tutorial has explored how pdb allows you to navigate through code with precision, set breakpoints, and inspect the state of your application at any moment. By leveraging pdb's powerful suite of commands, you can effectively identify bugs, understand program flow, and ensure your applications run as intended. Embracing these debugging techniques will not only boost your problem-solving skills but also significantly improve your development workflow.

What is QA Automation?

New To Automation?

Watch this FREE Masterclass and see automation in action!

 

➡️ Python In action

➡️ Selenium WebDriver In Action

➡️ API/Backend Testing In Action

➡️ Jenkins (CI/CD) use case demo

Get instant access to the webinar. Several videos show front-end & back-end automation in action using Python.