Debug With PDB

This document covers debugging Python programs using PDB interactive debugger including setting breakpoints, stepping through code, inspecting and modifying variables, and post-mortem debugging. Python's built-in interactive debugger.

This document explores debugging Python programs using PDB (Python DeBugger), the built-in interactive debugger that allows setting breakpoints, stepping through code, inspecting and modifying variables, evaluating expressions interactively, and performing post-mortem debugging after crashes.


Introduction

Imagine developing a Python application to analyze vast amounts of textual data for sentiment scores. As the application processes data, it occasionally encounters unexpected formats, causing crashes. Given the data volume and application complexity, identifying root causes using simple print() statements becomes increasingly challenging. This is where Python’s built-in interactive debugger, PDB, becomes essential.


What Is PDB

Definition

PDB stands for “Python DeBugger.” It is an interactive debugger for Python programs, providing capabilities to:

  • Set breakpoints at specific code locations
  • Step through code line by line
  • Inspect variable values at any point
  • Evaluate arbitrary Python expressions interactively
  • Modify variable values during execution
  • Perform post-mortem debugging after crashes

Advantages Over Print Debugging

FeaturePrint DebuggingPDB Debugger
Interactive inspectionNoYes
Modify variablesNoYes
Step through codeNoYes
Conditional executionNoYes
Stack inspectionNoYes
Post-mortem debuggingNoYes
Code modification neededYesMinimal

Setting Up PDB

Importing PDB

1import pdb

Setting Breakpoints

A breakpoint is a spot in code that tells PDB to pause execution at that specific line:

1import pdb
2
3def add_numbers(a, b):
4    pdb.set_trace()  # This will set a breakpoint in the code
5    result = a + b
6    return result
7
8print(add_numbers(3, 4))

Modern Breakpoint Syntax (Python 3.7+)

1def add_numbers(a, b):
2    breakpoint()  # Preferred method in Python 3.7+
3    result = a + b
4    return result
5
6print(add_numbers(3, 4))

Entering the Interactive Debugger

What Happens at Breakpoint

When a breakpoint is hit, execution pauses and an interactive debugger session starts:

1> /path/to/script.py(6)add_numbers()
2-> result = a + b
3(Pdb)

Debugger Prompt Explanation

ComponentMeaning
>Debugger prompt indicator
/path/to/script.pyCurrent file path
(6)Current line number
add_numbers()Current function name
-> result = a + bNext line to execute
(Pdb)Command prompt waiting for input

PDB Commands

CommandShortcutDescriptionExample
nextnExecute next line (don’t enter functions)n
stepsExecute line and enter functionss
continuecResume execution until next breakpointc
returnrContinue until current function returnsr
untiluntContinue until line greater than currentunt 10
jumpjJump to specific linej 15

Inspection Commands

CommandShortcutDescriptionExample
printpPrint expression valuep variable_name
ppppPretty-print expressionpp complex_dict
argsaShow function argumentsa
listlShow source code around current linel
longlistllShow entire function sourcell
wherewShow stack tracew
upuMove up one stack frameu
downdMove down one stack framed

Breakpoint Management Commands

CommandDescriptionExample
break / bSet breakpointb 10 (line 10)
tbreakSet temporary breakpointtbreak function_name
conditionAdd condition to breakpointcondition 1 x > 5
clearClear breakpointsclear 1
disableDisable breakpointdisable 1
enableEnable breakpointenable 1

Variable Modification Commands

CommandDescriptionExample
! prefixExecute Python statement!variable = 10
Direct assignmentModify variable!sentiment_score = 0.9

Control Commands

CommandShortcutDescription
quitqExit debugger and terminate program
exitExit debugger and terminate program
helphShow help for commands
aliasCreate command aliases

Stepping Through Code

Understanding Step vs Next

 1import pdb
 2
 3def helper_function(x):
 4    return x * 2
 5
 6def main_function(a, b):
 7    pdb.set_trace()
 8    result1 = helper_function(a)  # Line 9
 9    result2 = helper_function(b)  # Line 10
10    return result1 + result2
11
12print(main_function(3, 4))

Using Next (n)

1(Pdb) n
2> /script.py(10)main_function()
3-> result2 = helper_function(b)

Next executes line 9 completely without entering helper_function.

Using Step (s)

1(Pdb) s
2> /script.py(5)helper_function()
3-> return x * 2

Step enters into helper_function to debug it line by line.


Inspecting Variables

Basic Variable Inspection

1import pdb
2
3def calculate_sentiment(text):
4    words = text.split()
5    pdb.set_trace()
6    sentiment_score = len(words) * 0.1
7    return sentiment_score
8
9result = calculate_sentiment("This is a test sentence")

At the breakpoint:

 1(Pdb) p text
 2'This is a test sentence'
 3
 4(Pdb) p words
 5['This', 'is', 'a', 'test', 'sentence']
 6
 7(Pdb) p sentiment_score
 8*** NameError: name 'sentiment_score' is not defined
 9
10(Pdb) n
11> /script.py(7)calculate_sentiment()
12-> return sentiment_score
13
14(Pdb) p sentiment_score
150.5

Pretty Printing Complex Structures

 1import pdb
 2
 3def process_data():
 4    data = {
 5        'users': [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}],
 6        'settings': {'theme': 'dark', 'notifications': True}
 7    }
 8    pdb.set_trace()
 9    return data
10
11process_data()

At the breakpoint:

1(Pdb) p data
2{'users': [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}], 'settings': {'theme': 'dark', 'notifications': True}}
3
4(Pdb) pp data
5{'settings': {'notifications': True, 'theme': 'dark'},
6 'users': [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]}

Viewing Function Arguments

1import pdb
2
3def calculate_total(price, quantity, tax_rate):
4    pdb.set_trace()
5    subtotal = price * quantity
6    total = subtotal * (1 + tax_rate)
7    return total
8
9result = calculate_total(10.50, 3, 0.08)

At the breakpoint:

1(Pdb) a
2price = 10.5
3quantity = 3
4tax_rate = 0.08

Modifying Variables

Changing Variable Values

 1import pdb
 2
 3def process_score(score):
 4    pdb.set_trace()
 5    if score < 0:
 6        score = 0
 7    elif score > 100:
 8        score = 100
 9    return score
10
11result = process_score(150)

At the breakpoint:

 1(Pdb) p score
 2150
 3
 4(Pdb) !score = 95
 5
 6(Pdb) p score
 795
 8
 9(Pdb) c
1095

Testing Alternative Values

 1import pdb
 2
 3def calculate_discount(price, customer_type):
 4    pdb.set_trace()
 5    if customer_type == 'premium':
 6        discount = 0.20
 7    elif customer_type == 'standard':
 8        discount = 0.10
 9    else:
10        discount = 0.0
11
12    final_price = price * (1 - discount)
13    return final_price
14
15result = calculate_discount(100, 'standard')

At the breakpoint:

1(Pdb) p customer_type
2'standard'
3
4(Pdb) !customer_type = 'premium'
5
6(Pdb) c
7# Now calculates with premium discount

Working with Breakpoints

Setting Persistent Breakpoints

1import pdb
2
3def complex_function():
4    for i in range(10):
5        result = i * 2
6        print(result)
7
8complex_function()

Run with PDB:

1python -m pdb script.py

Set breakpoint at line 5:

1(Pdb) b 5
2Breakpoint 1 at /path/to/script.py:5
3
4(Pdb) c
5> /path/to/script.py(5)complex_function()
6-> result = i * 2

Conditional Breakpoints

1(Pdb) b 5, i > 5
2Breakpoint 2 at /path/to/script.py:5
3
4(Pdb) c
5# Only stops when i > 5

Managing Multiple Breakpoints

 1(Pdb) b
 2Num Type         Disp Enb   Where
 31   breakpoint   keep yes   at /path/to/script.py:5
 4    breakpoint already hit 6 times
 52   breakpoint   keep yes   at /path/to/script.py:5
 6    stop only if i > 5
 7
 8(Pdb) disable 1
 9(Pdb) enable 1
10(Pdb) clear 1
11Deleted breakpoint 1

Viewing Source Code

List Command

 1(Pdb) l
 2  1     import pdb
 3  2
 4  3     def calculate_total(items):
 5  4         pdb.set_trace()
 6  5  ->     total = 0
 7  6         for item in items:
 8  7             total += item['price']
 9  8         return total
10  9
11 10     result = calculate_total([{'price': 10}, {'price': 20}])
12
13(Pdb) l 1, 20
14# Shows lines 1-20

Long List Command

1(Pdb) ll
2  3     def calculate_total(items):
3  4         pdb.set_trace()
4  5  ->     total = 0
5  6         for item in items:
6  7             total += item['price']
7  8         return total
8# Shows entire function

Stack Navigation

Viewing Call Stack

 1import pdb
 2
 3def level_3():
 4    pdb.set_trace()
 5    return "Level 3"
 6
 7def level_2():
 8    return level_3()
 9
10def level_1():
11    return level_2()
12
13level_1()

At the breakpoint:

1(Pdb) w
2  /script.py(11)<module>()
3-> level_1()
4  /script.py(9)level_1()
5-> return level_2()
6  /script.py(6)level_2()
7-> return level_3()
8> /script.py(4)level_3()
9-> return "Level 3"

Moving Through Stack Frames

 1(Pdb) up
 2> /script.py(6)level_2()
 3-> return level_3()
 4
 5(Pdb) up
 6> /script.py(9)level_1()
 7-> return level_2()
 8
 9(Pdb) down
10> /script.py(6)level_2()
11-> return level_3()

Post-Mortem Debugging

Running Script with PDB

When a program crashes, use PDB to inspect its state at crash time:

1python -m pdb script.py

If an exception occurs, PDB automatically starts and allows inspection.

Example Crash Scenario

 1# script.py
 2def divide_numbers(a, b):
 3    return a / b
 4
 5def main():
 6    result = divide_numbers(10, 0)
 7    print(result)
 8
 9if __name__ == '__main__':
10    main()

Run with PDB:

1python -m pdb script.py

Output:

 1> /path/to/script.py(1)<module>()
 2-> def divide_numbers(a, b):
 3(Pdb) c
 4Traceback (most recent call last):
 5  File "/usr/lib/python3.9/pdb.py", line 1723, in main
 6    pdb._runscript(mainpyfile)
 7  ...
 8ZeroDivisionError: division by zero
 9Uncaught exception. Entering post mortem debugging
10> /path/to/script.py(3)divide_numbers()
11-> return a / b
12(Pdb) p a
1310
14(Pdb) p b
150

Post-Mortem Function

Alternatively, call pdb.post_mortem() after exception:

 1import pdb
 2import sys
 3import traceback
 4
 5try:
 6    result = 10 / 0
 7except:
 8    type, value, tb = sys.exc_info()
 9    traceback.print_exc()
10    pdb.post_mortem(tb)

Practical Debugging Session

Complete Example

 1import pdb
 2
 3def analyze_sentiment(text):
 4    """Analyze sentiment of text."""
 5    words = text.split()
 6
 7    # Calculate sentiment score
 8    positive_words = ['good', 'great', 'excellent', 'happy']
 9    negative_words = ['bad', 'terrible', 'awful', 'sad']
10
11    pdb.set_trace()  # Set breakpoint here
12
13    positive_count = sum(1 for word in words if word.lower() in positive_words)
14    negative_count = sum(1 for word in words if word.lower() in negative_words)
15
16    if len(words) == 0:
17        return 0.0
18
19    sentiment_score = (positive_count - negative_count) / len(words)
20    return sentiment_score
21
22# Test the function
23text = "This is a great and excellent day"
24result = analyze_sentiment(text)
25print(f"Sentiment score: {result}")

Debugging Session Commands

 1> /script.py(14)analyze_sentiment()
 2-> positive_count = sum(1 for word in words if word.lower() in positive_words)
 3
 4(Pdb) p words
 5['This', 'is', 'a', 'great', 'and', 'excellent', 'day']
 6
 7(Pdb) p positive_words
 8['good', 'great', 'excellent', 'happy']
 9
10(Pdb) n
11> /script.py(15)analyze_sentiment()
12-> negative_count = sum(1 for word in words if word.lower() in negative_words)
13
14(Pdb) p positive_count
152
16
17(Pdb) n
18> /script.py(17)analyze_sentiment()
19-> if len(words) == 0:
20
21(Pdb) p negative_count
220
23
24(Pdb) c
25Sentiment score: 0.2857142857142857

Best Practices

When to Use PDB

ScenarioUse PDBAlternative
Complex logic bugsYesLogging
Intermittent issuesYesLogging
Production debuggingNoLogging
Quick value checksNoPrint statements
Algorithm verificationYesUnit tests
Understanding code flowYesCode review

Tips for Effective PDB Usage

  • Start with targeted breakpoints near suspected problem areas
  • Use conditional breakpoints to catch specific scenarios
  • Combine PDB with logging for comprehensive debugging
  • Document debugging sessions for future reference
  • Remove or comment out breakpoints before committing code
  • Use breakpoint() (Python 3.7+) for cleaner syntax

Common Pitfalls

PitfallProblemSolution
Too many breakpointsSlows debuggingUse conditional breakpoints
Forgetting breakpoints in codeProduction issuesCode review process
Not using llMissing contextUse ll to see full function
Ignoring stack tracesMissing call chainUse w command regularly

Advanced PDB Features

Creating Command Aliases

1(Pdb) alias pl pp locals()
2(Pdb) pl
3# Pretty-prints all local variables

Using .pdbrc Configuration File

Create .pdbrc in home directory:

1# ~/.pdbrc
2# Aliases
3alias pl pp locals()
4alias pg pp globals()
5alias pd pp dir()
6
7# Auto-execute commands
8c

Interactive Python Evaluation

1(Pdb) !import math
2(Pdb) !result = math.sqrt(16)
3(Pdb) p result
44.0

Key Takeaways

PDB is Python’s built-in interactive debugger for diagnosing and resolving issues in Python applications. With PDB, breakpoints can be set, code can be stepped through, variables can be inspected and modified, and Python expressions can be evaluated interactively. PDB allows for in-depth understanding of code execution flow and aids in identifying root causes more efficiently than traditional print-based debugging. Post-mortem debugging enables inspection of program state after crashes, making it invaluable for diagnosing production issues.


Conclusion

PDB provides powerful interactive debugging capabilities built into Python. By setting strategic breakpoints, stepping through code execution, inspecting and modifying variables in real-time, and performing post-mortem analysis of crashes, developers can diagnose complex issues that would be difficult or impossible to debug with print statements alone. Mastering PDB commands and workflows significantly enhances debugging efficiency and code understanding.


FAQ