PDB
PDB is Python's built-in interactive debugger, designed for developers who need powerful, script-friendly debugging without external dependencies. It provides a command-line interface for inspecting code, setting breakpoints, stepping through execution, and evaluating expressions—all directly from your terminal or within any Python environment.
It enables you to:
- Pause execution at any point using
pdb.set_trace()or the modernbreakpoint() - Step through code line by line to observe program flow
- Inspect and modify variables in real time
- Set, clear, and list breakpoints (including conditional breakpoints)
- Evaluate Python expressions interactively
- Navigate the call stack and examine function contexts
Basics
Starting the Debugger
Method 1: Insert Breakpoint in Code
import pdb
pdb.set_trace() # Execution will pause here
Method 2: Run Script with PDB
python -m pdb script.py
Method 3: Post-mortem Debugging
import pdb
try:
# your code here
pass
except:
pdb.post_mortem()
Method 4: Python 3.7+ breakpoint() Function
breakpoint() # Equivalent to pdb.set_trace()
PDB Commands 🔖
# Navigation and flow control
n # Execute next line within current function
s # Step into functions (go deeper into calls)
c # Continue execution until next breakpoint
unt # Continue until line greater than current is reached
r # Continue until current function returns
j <line> # Jump to specific line number (within current function)
# Stack navigation
u # Move up one frame in stack trace
d # Move down one frame in stack trace
w # Print current stack trace with frame numbers
bt # Same as w, print stack trace
# Breakpoint management
b # List all breakpoints
b <line> # Set breakpoint at line number in current file
b <file:line> # Set breakpoint at specific file and line
b <function> # Set breakpoint at function entry
b <line>, <condition> # Set conditional breakpoint
tbreak <line> # Set temporary breakpoint (removed after first hit)
tbreak <function> # Set temporary breakpoint at function
cl # Clear all breakpoints
cl <number> # Clear specific breakpoint by number
cl <file:line> # Clear breakpoint at specific location
disable <number> # Disable breakpoint (keep but don't trigger)
enable <number> # Re-enable disabled breakpoint
ignore <number> [count] # Ignore breakpoint for next N hits
condition <number> <condition> # Add/change condition for breakpoint
commands <number> # Execute commands when breakpoint hits
# Code display
l # Show current code around current line (11 lines)
l <first>, <last> # Show specific line range
l <line> # Show code around specific line
ll # Show full source code for current function
source <object> # Show source code for object (function, class, etc.)
# Variable inspection
p <expression> # Print value of expression
pp <expression> # Pretty-print value of expression
whatis <expression> # Print type of expression
type <expression> # Print type information
a # Print arguments of current function
rv # Print return value of last returned function
# Object and namespace inspection
locals # Print all local variables
globals # Print all global variables
dir() # List names in current namespace
dir(<object>) # List attributes of object
vars() # Show local variables as dictionary
vars(<object>) # Show object's attributes as dictionary
# Code execution
!<statement> # Execute arbitrary Python statement
interact # Start interactive Python interpreter
exec <code> # Execute Python code
eval <expression> # Evaluate Python expression and print result
# System commands
q # Quit debugger and terminate program
exit # Same as q
restart # Restart the debugged program
run [args] # Restart program with optional command line arguments
# Debugging control
debug <code> # Enter recursive debugger to debug code
undebug # Exit recursive debugger
retval # Show return value of most recently returned function
# Help and information
h # Show help message
h <command> # Show help for specific command
? # Same as h
? <command> # Same as h <command>
# Aliases and customization
alias [name [command]] # Create, list, or show command aliases
unalias <name> # Remove an alias
display <expr> # Auto-display expression value
undisplay <expr> # Stop auto-displaying specific expression
undisplay # Clear all auto-displays
# Special variables
__return__ # Return value of current function (when at return)
__exception__ # Current exception being handled
__args__ # Arguments passed to current function
# Command shortcuts
<Enter> # Repeat last command
!! # Execute last shell command
!history # Show command history
;; # Command separator (execute multiple commands)
# Exception and post-mortem commands
post_mortem() # Enter post-mortem debugging mode
post_mortem(tb) # Debug with specific traceback
pm() # Short alias for post_mortem
exceptions # Navigate exception chains (Python 3.3+)
Example Command Usage
# Basic debugging session examples
(Pdb) l # List current code
(Pdb) b 25 # Set breakpoint at line 25
(Pdb) b calculate, x > 10 # Conditional breakpoint
(Pdb) c # Continue to breakpoint
(Pdb) p x, y, z # Print multiple variables
(Pdb) pp complex_dict # Pretty-print complex data
(Pdb) w # Show stack trace
(Pdb) u # Move up stack frame
(Pdb) !x = 42 # Modify variable
(Pdb) interact # Start Python session
>>> # Do complex operations
>>> exit() # Return to PDB
(Pdb) r # Return from function
(Pdb) cl 1 # Clear breakpoint 1
(Pdb) q # Quit debugger
Command Patterns and Tips
Efficient Debugging Patterns:
- Use
llto see full function context - Combine
wandlto understand location - Use
ppfor complex data structures - Set conditional breakpoints to avoid noise
- Use
interactfor complex variable manipulation - Use
!prefix for variable assignments and complex operations
Advanced PDB Features
Conditional Breakpoints
# Set breakpoint that only triggers when condition is true
(Pdb) b 10, x > 5
Temporary Breakpoints
# Set breakpoint that's automatically removed after first hit
(Pdb) tbreak 15
Debugging with Context
import pdb
import sys
def debug_trace():
"""Set a tracepoint in the Python debugger that works with Qt"""
from pdb import set_trace
set_trace()
# Usage in code
debug_trace()
Configuration
Customize your PDB debugging experience with configuration files, aliases, and advanced setup options.
.pdbrc Configuration File
Create a .pdbrc file in your home directory to customize PDB with aliases and startup commands.
Basic .pdbrc Setup
# ~/.pdbrc
# Custom aliases for common debugging tasks
alias dt pp dir(%1)
alias ps pp sorted(dir(%1))
alias loc pp locals()
alias glob pp globals()
alias cls !import os; os.system('clear')
Advanced .pdbrc Examples
# ~/.pdbrc - Advanced configuration
# Pretty print shortcuts
alias ll longlist
alias ps pp sorted(locals())
alias pg pp globals()
# Inspection aliases
alias dt pp dir(%1)
alias at pp [a for a in dir(%1) if not a.startswith('_')]
alias st pp type(%1)
# Navigation helpers
alias n next
alias s step
alias c continue
alias q quit
# Debugging helpers
alias trace_calls !import sys; sys.settrace(lambda f, e, a: print(f"Call: {f.f_code.co_filename}:{f.f_lineno} {f.f_code.co_name}") if e == 'call' else None)
alias trace_off !import sys; sys.settrace(None)
# Clear screen
alias cls !import os; os.system('clear' if os.name == 'posix' else 'cls')
# Show current location with more context
alias where w; l
# JSON pretty print
alias pj !import json; json.dumps(%1, indent=2) if hasattr(%1, '__dict__') or isinstance(%1, (dict, list)) else str(%1)
Framework-Specific .pdbrc
Django Configuration
# ~/.pdbrc - Django specific
alias dj_settings pp __import__('django.conf').conf.settings
alias dj_urls !from django.core.management import execute_from_command_line; execute_from_command_line(['', 'show_urls'])
alias dj_shell !from django.core.management import execute_from_command_line; execute_from_command_line(['', 'shell'])
Flask Configuration
# ~/.pdbrc - Flask specific
alias flask_app pp app if 'app' in locals() else 'No Flask app in scope'
alias flask_config pp app.config if 'app' in locals() else 'No Flask app in scope'
alias flask_ctx pp g.__dict__ if 'g' in locals() else 'No Flask context'
Environment-Based Debug Activation
Using Environment Variables
import os
def conditional_debug():
"""Only start debugger if DEBUG environment variable is set"""
if os.getenv('DEBUG') == '1':
import pdb; pdb.set_trace()
# Usage
conditional_debug()
Debug Decorator
import functools
import os
def debug_on_error(func):
"""Decorator that starts debugger on exceptions"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
if os.getenv('DEBUG_ON_ERROR') == '1':
import pdb; pdb.post_mortem()
raise
return wrapper
# Usage
@debug_on_error
def my_function():
# Your code here
pass
Advanced Configuration Patterns
Context Manager for Clean Debugging
import contextlib
import pdb
@contextlib.contextmanager
def debug_context(condition=True):
"""Context manager for conditional debugging"""
if condition:
print("Entering debug mode...")
pdb.set_trace()
try:
yield
finally:
if condition:
print("Exiting debug mode...")
# Usage
with debug_context(some_condition):
# Code to debug
pass
Custom Debugger Class
import pdb
import sys
class CustomDebugger(pdb.Pdb):
"""Enhanced debugger with custom features"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.debug_count = 0
def set_trace(self, frame=None):
"""Enhanced set_trace with logging"""
self.debug_count += 1
print(f"Debug session #{self.debug_count} started")
super().set_trace(frame or sys._getframe().f_back)
def do_vars(self, arg):
"""Custom command to show variables"""
frame = self.curframe
print("Local variables:")
for name, value in frame.f_locals.items():
print(f" {name} = {repr(value)}")
# Usage
debugger = CustomDebugger()
debugger.set_trace()
Debugging Configuration Class
class DebugConfig:
"""Centralized debugging configuration"""
def __init__(self):
self.enabled = os.getenv('DEBUG', '0') == '1'
self.break_on_exception = os.getenv('DEBUG_ON_ERROR', '0') == '1'
self.log_calls = os.getenv('DEBUG_TRACE_CALLS', '0') == '1'
def breakpoint(self, condition=True):
"""Conditional breakpoint"""
if self.enabled and condition:
import pdb; pdb.set_trace()
def post_mortem_on_exception(self, func):
"""Decorator for post-mortem debugging"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception:
if self.break_on_exception:
import pdb; pdb.post_mortem()
raise
return wrapper
# Global instance
debug = DebugConfig()
# Usage
debug.breakpoint(x > 10) # Only breaks if x > 10 and debug enabled
IDE Integration Configuration
VS Code Settings
{
"python.terminal.activateEnvironment": true,
"python.debugging.console": "internalConsole",
"python.testing.pytestArgs": ["--pdb"],
"python.linting.enabled": true
}
Jupyter Notebook Configuration
# In Jupyter cell
%pdb on # Enable automatic debugger on exceptions
%pdb off # Disable automatic debugger
# Manual breakpoint in notebook
import pdb; pdb.set_trace()
Performance Considerations
Lightweight Debug Helper
import os
class LightDebug:
"""Lightweight debugging helper"""
def __init__(self):
self.enabled = os.getenv('DEBUG') == '1'
def trace(self, *args):
"""Print trace information only if debugging enabled"""
if self.enabled:
print(f"DEBUG: {' '.join(str(arg) for arg in args)}")
def breakpoint(self, condition=True):
"""Conditional breakpoint"""
if self.enabled and condition:
import pdb; pdb.set_trace()
# Global instance
dbg = LightDebug()
# Usage
dbg.trace("Variable x =", x)
dbg.breakpoint(x > threshold)
Security Considerations
Production Safety
import os
def safe_debug():
"""Only allow debugging in development environments"""
if os.getenv('ENVIRONMENT') not in ['development', 'staging']:
print("Debugging disabled in production")
return
import pdb; pdb.set_trace()
# Usage in production-deployed code
safe_debug() # Won't start debugger in production
Configuration Best Practices
- Use environment variables for debug control
- Create aliases for frequently used commands
- Separate configuration by environment (dev/staging/prod)
- Document custom commands in your .pdbrc file
- Version control your .pdbrc for team sharing
- Test configurations across different environments
Tutorials
Hands-on tutorials to master Python debugging with PDB through practical examples and step-by-step walkthroughs.
Tutorial 1: Step-by-Step Basic Debugging
Learn the fundamentals of PDB debugging with a simple statistics function.
Example: Debugging a Simple Function
# tutorial_basic.py
def calculate_statistics(data):
import pdb; pdb.set_trace()
if not data:
return None
total = sum(data)
count = len(data)
mean = total / count
# Calculate median
sorted_data = sorted(data)
n = len(sorted_data)
if n % 2 == 0:
median = (sorted_data[n//2 - 1] + sorted_data[n//2]) / 2
else:
median = sorted_data[n//2]
return {"mean": mean, "median": median, "count": count}
# Test the function
result = calculate_statistics([4, 2, 7, 1, 9, 5])
print(f"Statistics: {result}")
Complete Debugging Walkthrough
$ python tutorial_basic.py
> tutorial_basic.py(4)calculate_statistics()
-> if not data:
(Pdb) p data
[4, 2, 7, 1, 9, 5]
(Pdb) l
1 # tutorial_basic.py
2 def calculate_statistics(data):
3 import pdb; pdb.set_trace()
4 -> if not data:
5 return None
6
7 total = sum(data)
8 count = len(data)
9 mean = total / count
10
(Pdb) n
> tutorial_basic.py(7)calculate_statistics()
-> total = sum(data)
(Pdb) n
> tutorial_basic.py(8)calculate_statistics()
-> count = len(data)
(Pdb) p total
28
(Pdb) n
> tutorial_basic.py(9)calculate_statistics()
-> mean = total / count
(Pdb) p count
6
(Pdb) n
> tutorial_basic.py(12)calculate_statistics()
-> sorted_data = sorted(data)
(Pdb) p mean
4.666666666666667
(Pdb) s # Step into sorted() - won't work as it's built-in
> tutorial_basic.py(13)calculate_statistics()
-> n = len(sorted_data)
(Pdb) p sorted_data
[1, 2, 4, 5, 7, 9]
(Pdb) n
> tutorial_basic.py(14)calculate_statistics()
-> if n % 2 == 0:
(Pdb) p n
6
(Pdb) pp n % 2 # Pretty print expression
0
(Pdb) n
> tutorial_basic.py(15)calculate_statistics()
-> median = (sorted_data[n//2 - 1] + sorted_data[n//2]) / 2
(Pdb) p n//2
3
(Pdb) p sorted_data[n//2 - 1]
5
(Pdb) p sorted_data[n//2]
7
(Pdb) n
> tutorial_basic.py(19)calculate_statistics()
-> return {"mean": mean, "median": median, "count": count}
(Pdb) p median
6.0
(Pdb) c
Statistics: {'mean': 4.666666666666667, 'median': 6.0, 'count': 6}
Tutorial 2: Advanced Breakpoint Management
Master breakpoint techniques including conditional breakpoints and dynamic debugging.
Dynamic Breakpoint Setting
# tutorial_breakpoints.py
def complex_algorithm(data, threshold=10):
results = []
for i, value in enumerate(data):
# Dynamic processing
if value > threshold:
processed = value * 2
else:
processed = value + 5
results.append(processed)
# Complex condition we want to debug
if processed > 15 and i % 2 == 0:
final_result = processed ** 2
else:
final_result = processed
results[-1] = final_result
return results
# Test data
test_data = [3, 12, 7, 20, 1, 15, 8]
result = complex_algorithm(test_data, threshold=10)
print(f"Result: {result}")
Advanced Breakpoint Session
$ python -m pdb tutorial_breakpoints.py
(Pdb) l
1 # tutorial_breakpoints.py
2 def complex_algorithm(data, threshold=10):
3 results = []
4
5 for i, value in enumerate(data):
6 # Dynamic processing
7 if value > threshold:
8 processed = value * 2
9 else:
10 processed = value + 5
(Pdb) b 8 # Set breakpoint at line 8
Breakpoint 1 at tutorial_breakpoints.py:8
(Pdb) b 10 # Set breakpoint at line 10
Breakpoint 2 at tutorial_breakpoints.py:10
(Pdb) b 16, processed > 15 # Conditional breakpoint
Breakpoint 3 at tutorial_breakpoints.py:16
(Pdb) c
> tutorial_breakpoints.py:10 (complex_algorithm())
-> processed = value + 5
(Pdb) p value
3
(Pdb) c
> tutorial_breakpoints.py:8 (complex_algorithm())
-> processed = value * 2
(Pdb) p value
12
(Pdb) c
> tutorial_breakpoints.py:10 (complex_algorithm())
-> processed = value + 5
(Pdb) p value
7
(Pdb) c
> tutorial_breakpoints.py:8 (complex_algorithm())
-> processed = value * 2
(Pdb) p value
20
(Pdb) n
> tutorial_breakpoints.py:13 (complex_algorithm())
-> results.append(processed)
(Pdb) p processed
40
(Pdb) c
> tutorial_breakpoints.py:16 (complex_algorithm())
-> if processed > 15 and i % 2 == 0:
(Pdb) p processed, i
(40, 3)
(Pdb) cl # Clear all breakpoints
Deleted breakpoints 1 2 3
(Pdb) tbreak 17 # Temporary breakpoint
Breakpoint 4 at tutorial_breakpoints.py:17
(Pdb) c
> tutorial_breakpoints.py:17 (complex_algorithm())
-> final_result = processed ** 2
(Pdb) p final_result # Variable doesn't exist yet
*** NameError: name 'final_result' is not defined
(Pdb) n
> tutorial_breakpoints.py:21 (complex_algorithm())
-> results[-1] = final_result
(Pdb) p final_result
1600
(Pdb) c
Result: [8, 24, 12, 1600, 6, 30, 13]
Tutorial 3: Stack Navigation and Function Calls
Learn to navigate call stacks and debug multi-level function calls.
Multi-level Function Debugging
# tutorial_stack.py
def level_1_function(data):
print("Entering level 1")
return level_2_function(data * 2)
def level_2_function(data):
print("Entering level 2")
result = level_3_function(data + 10)
return result * 3
def level_3_function(data):
print("Entering level 3")
import pdb; pdb.set_trace()
if data > 50:
calculation = data / 2
else:
calculation = data * 1.5
return calculation
# Start the chain
final_result = level_1_function(15)
print(f"Final result: {final_result}")
Stack Navigation Tutorial
$ python tutorial_stack.py
Entering level 1
Entering level 2
Entering level 3
> tutorial_stack.py(15)level_3_function()
-> if data > 50:
(Pdb) w # Show where we are (stack trace)
tutorial_stack.py(25)<module>()
-> final_result = level_1_function(15)
tutorial_stack.py(4)level_1_function()
-> return level_2_function(data * 2)
tutorial_stack.py(8)level_2_function()
-> result = level_3_function(data + 10)
> tutorial_stack.py(15)level_3_function()
-> if data > 50:
(Pdb) u # Move up one frame
> tutorial_stack.py(8)level_2_function()
-> result = level_3_function(data + 10)
(Pdb) p data
40
(Pdb) u # Move up another frame
> tutorial_stack.py(4)level_1_function()
-> return level_2_function(data * 2)
(Pdb) p data
15
(Pdb) u # Move to top level
> tutorial_stack.py(25)<module>()
-> final_result = level_1_function(15)
(Pdb) l
20 return calculation
21
22 # Start the chain
23 final_result = level_1_function(15)
24 -> print(f"Final result: {final_result}")
25
(Pdb) d # Move down one frame
> tutorial_stack.py(4)level_1_function()
-> return level_2_function(data * 2)
(Pdb) d # Move down again
> tutorial_stack.py(8)level_2_function()
-> result = level_3_function(data + 10)
(Pdb) d # Back to our original frame
> tutorial_stack.py(15)level_3_function()
-> if data > 50:
(Pdb) p data # data in current frame
40
(Pdb) !data = 60 # Modify variable
(Pdb) p data
60
(Pdb) c
Final result: 90.0
Tutorial 4: Advanced Expression Evaluation and Manipulation
Master interactive debugging with complex object manipulation and expression evaluation.
Interactive Python Session
# tutorial_interactive.py
class DataProcessor:
def __init__(self, data):
self.data = data
self.processed = []
self.stats = {}
def process(self):
import pdb; pdb.set_trace()
for item in self.data:
if isinstance(item, (int, float)):
processed_item = self.transform_number(item)
elif isinstance(item, str):
processed_item = self.transform_string(item)
else:
processed_item = str(item)
self.processed.append(processed_item)
self.calculate_stats()
return self.processed
def transform_number(self, num):
return num * 2 if num > 0 else abs(num)
def transform_string(self, text):
return text.upper().strip()
def calculate_stats(self):
numbers = [x for x in self.processed if isinstance(x, (int, float))]
self.stats = {
'count': len(numbers),
'sum': sum(numbers) if numbers else 0,
'avg': sum(numbers) / len(numbers) if numbers else 0
}
# Test the processor
processor = DataProcessor([1, -5, "hello", 3.14, " world ", 0])
result = processor.process()
print(f"Processed: {result}")
print(f"Stats: {processor.stats}")
Interactive Debugging Session
$ python tutorial_interactive.py
> tutorial_interactive.py(9)process()
-> for item in self.data:
(Pdb) p self.data
[1, -5, 'hello', 3.14, ' world ', 0]
(Pdb) interact # Start interactive Python session
*interactive*
>>> self.data
[1, -5, 'hello', 3.14, ' world ', 0]
>>> # We can do complex operations here
>>> import math
>>> [math.sqrt(abs(x)) if isinstance(x, (int, float)) else len(x) for x in self.data]
[1.0, 2.23606797749979, 5, 1.772004514666935, 9, 0.0]
>>> # Modify the object
>>> self.data.append(42)
>>> self.data
[1, -5, 'hello', 3.14, ' world ', 0, 42]
>>> exit() # Exit interactive mode
(Pdb) p self.data # Data is modified
[1, -5, 'hello', 3.14, ' world ', 0, 42]
(Pdb) # Let's set up some debugging aids
(Pdb) !debug_info = lambda: print(f"Current item: {item}, Type: {type(item)}, Processed so far: {len(self.processed)}")
(Pdb) n
> tutorial_interactive.py(10)process()
-> if isinstance(item, (int, float)):
(Pdb) p item
1
(Pdb) !debug_info()
Current item: 1, Type: <class 'int'>, Processed so far: 0
(Pdb) s # Step into isinstance
> tutorial_interactive.py(11)process()
-> processed_item = self.transform_number(item)
(Pdb) s # Step into transform_number
> tutorial_interactive.py(21)transform_number()
-> return num * 2 if num > 0 else abs(num)
(Pdb) p num
1
(Pdb) !result = num * 2 if num > 0 else abs(num) # Manually execute
(Pdb) p result
2
(Pdb) r # Return from current function
--Return--
> tutorial_interactive.py(21)transform_number()->2
-> return num * 2 if num > 0 else abs(num)
(Pdb) p retval # Special variable containing return value
2
(Pdb) c
Processed: [2, 5, 'HELLO', 6.28, 'WORLD', 0, 84]
Stats: {'count': 5, 'sum': 97.28, 'avg': 19.456}
Tutorial 5: Debugging Complex Data Structures
Learn to navigate and debug complex nested data structures effectively.
Working with Nested Data
# tutorial_complex_data.py
def analyze_nested_data(data_structure):
import pdb; pdb.set_trace()
results = {
'lists': [],
'dicts': [],
'scalars': [],
'nested_count': 0
}
def process_item(item, path="root"):
if isinstance(item, dict):
results['dicts'].append((path, len(item)))
for key, value in item.items():
new_path = f"{path}.{key}"
process_item(value, new_path)
elif isinstance(item, list):
results['lists'].append((path, len(item)))
for i, value in enumerate(item):
new_path = f"{path}[{i}]"
process_item(value, new_path)
else:
results['scalars'].append((path, item))
process_item(data_structure)
return results
# Complex nested structure
complex_data = {
'users': [
{'name': 'Alice', 'age': 30, 'hobbies': ['reading', 'swimming']},
{'name': 'Bob', 'age': 25, 'hobbies': ['gaming', 'cooking', 'hiking']}
],
'settings': {
'theme': 'dark',
'notifications': {
'email': True,
'push': False,
'sms': {'enabled': True, 'number': '+1234567890'}
}
},
'version': '1.0.0'
}
result = analyze_nested_data(complex_data)
print(f"Analysis: {result}")
Complex Data Debugging Session
$ python tutorial_complex_data.py
> tutorial_complex_data.py(4)analyze_nested_data()
-> results = {
(Pdb) pp data_structure # Pretty print the complex structure
{'settings': {'notifications': {'email': True,
'push': False,
'sms': {'enabled': True,
'number': '+1234567890'}},
'theme': 'dark'},
'users': [{'age': 30, 'hobbies': ['reading', 'swimming'], 'name': 'Alice'},
{'age': 25,
'hobbies': ['gaming', 'cooking', 'hiking'],
'name': 'Bob'}],
'version': '1.0.0'}
(Pdb) n
> tutorial_complex_data.py(10)analyze_nested_data()
-> def process_item(item, path="root"):
(Pdb) b 12 # Set breakpoint in nested function
Breakpoint 1 at tutorial_complex_data.py:12
(Pdb) b 18 # Set breakpoint for list processing
Breakpoint 2 at tutorial_complex_data.py:18
(Pdb) c
> tutorial_complex_data.py(12)process_item()
-> if isinstance(item, dict):
(Pdb) p item.keys()
dict_keys(['users', 'settings', 'version'])
(Pdb) p path
'root'
(Pdb) c
> tutorial_complex_data.py:12 process_item()
-> if isinstance(item, dict):
(Pdb) p path
'root.users'
(Pdb) c
> tutorial_complex_data.py:18 process_item()
-> results['lists'].append((path, len(item)))
(Pdb) p path, len(item)
('root.users', 2)
(Pdb) c
> tutorial_complex_data.py:12 process_item()
-> if isinstance(item, dict):
(Pdb) p path
'root.users[0]'
(Pdb) # Let's examine the current results so far
(Pdb) pp results
{'dicts': [('root', 3), ('root.users[0]', 3)],
'lists': [('root.users', 2)],
'nested_count': 0,
'scalars': []}
(Pdb) cl # Clear breakpoints to speed up
Deleted breakpoints 1 2
(Pdb) c
Analysis: {'lists': [('root.users', 2), ('root.users[0].hobbies', 2), ('root.users[1].hobbies', 3)], 'dicts': [('root', 3), ('root.users[0]', 3), ('root.users[1]', 3), ('root.settings', 2), ('root.settings.notifications', 3), ('root.settings.notifications.sms', 2)], 'scalars': [('root.users[0].name', 'Alice'), ('root.users[0].age', 30), ('root.users[0].hobbies[0]', 'reading'), ('root.users[0].hobbies[1]', 'swimming'), ('root.users[1].name', 'Bob'), ('root.users[1].age', 25), ('root.users[1].hobbies[0]', 'gaming'), ('root.users[1].hobbies[1]', 'cooking'), ('root.users[1].hobbies[2]', 'hiking'), ('root.settings.theme', 'dark'), ('root.settings.notifications.email', True), ('root.settings.notifications.push', False), ('root.settings.notifications.sms.enabled', True), ('root.settings.notifications.sms.number', '+1234567890'), ('root.version', '1.0.0')], 'nested_count': 0}
Tutorial 6: Post-mortem Debugging
Master debugging after exceptions occur, examining crash states and error conditions.
Debugging After Exceptions
# tutorial_postmortem.py
import pdb
def risky_calculation(data):
results = []
for i, value in enumerate(data):
try:
# Potentially problematic operations
if isinstance(value, str):
numeric_value = float(value)
else:
numeric_value = value
# Division that might fail
result = 100 / numeric_value
# Index that might be out of bounds
next_value = data[i + 1] if i + 1 < len(data) else 0
results.append(result + next_value)
except Exception as e:
print(f"Error at index {i}: {e}")
# Enter post-mortem debugging
pdb.post_mortem()
return results
# Test with problematic data
problematic_data = [5, "10", 0, "invalid", 2.5]
try:
result = risky_calculation(problematic_data)
print(f"Results: {result}")
except Exception:
print("Unhandled exception occurred")
pdb.post_mortem()
Post-mortem Debugging Session
$ python tutorial_postmortem.py
Error at index 2: float division by zero
> tutorial_postmortem.py(12)risky_calculation()
-> result = 100 / numeric_value
(Pdb) p i, value, numeric_value
(2, 0, 0)
(Pdb) p data
[5, '10', 0, 'invalid', 2.5]
(Pdb) # We can examine the state when the error occurred
(Pdb) p results
[20.0, 10.0]
(Pdb) # Check what the next value would have been
(Pdb) p data[i + 1] if i + 1 < len(data) else 0
'invalid'
(Pdb) # Exit this post-mortem session
(Pdb) c
Error at index 3: could not convert string to float: invalid
> tutorial_postmortem.py(8)risky_calculation()
-> numeric_value = float(value)
(Pdb) p value
'invalid'
(Pdb) # Try to fix the value interactively
(Pdb) !numeric_value = 0 # Set a default value
(Pdb) n
> tutorial_postmortem.py(12)risky_calculation()
-> result = 100 / numeric_value
(Pdb) p numeric_value
0
(Pdb) # This would cause another division by zero
(Pdb) !numeric_value = 1 # Set a safe value
(Pdb) n
> tutorial_postmortem.py(15)risky_calculation()
-> next_value = data[i + 1] if i + 1 < len(data) else 0
(Pdb) p result
100.0
(Pdb) c
Results: [20.0, 10.0, 102.5]
Integration
Integrate PDB debugging into your development workflow with IDE configurations, testing frameworks, and deployment environments.
IDE Integration
Integrate PDB with your IDE to streamline debugging workflows. IDE integration allows you to launch debugging sessions directly from your editor, set breakpoints visually, and manage debugging configurations through familiar interfaces.
VS Code Integration
VS Code provides excellent Python debugging support with launch configurations and custom tasks. These configurations allow you to debug Python files and tests with PDB while maintaining full IDE integration for breakpoint management and variable inspection.
Configuration for PDB
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File with PDB",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"env": {
"PYTHONBREAKPOINT": "pdb.set_trace"
}
},
{
"name": "Python: Debug Tests",
"type": "python",
"request": "launch",
"module": "pytest",
"args": ["${workspaceFolder}/tests/", "-s", "--pdb"],
"console": "integratedTerminal"
}
]
}
VS Code Tasks for Debugging
// .vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Debug with PDB",
"type": "shell",
"command": "python",
"args": ["-m", "pdb", "${file}"],
"group": "test",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "new"
},
"problemMatcher": []
},
{
"label": "Debug Tests with PDB",
"type": "shell",
"command": "python",
"args": ["-m", "pytest", "${workspaceFolder}/tests/", "-s", "--pdb"],
"group": "test",
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "new"
},
"problemMatcher": []
}
]
}
PyCharm Integration
PyCharm's external tools feature allows you to integrate PDB debugging directly into your IDE workflow. This setup enables you to launch PDB sessions for any file with a simple menu command or keyboard shortcut.
Custom External Tool for PDB
<!-- Settings > Tools > External Tools -->
<tool name="PDB Debug"
description="Debug current file with PDB"
showInMainMenu="true"
showInEditor="true"
showInProject="false"
showInSearchPopup="false"
disabled="false"
useConsole="true"
showConsoleOnStdOut="true"
showConsoleOnStdErr="true"
synchronizeAfterRun="false">
<exec>
<option name="COMMAND" value="python" />
<option name="PARAMETERS" value="-m pdb $FilePath$" />
<option name="WORKING_DIRECTORY" value="$ProjectFileDir$" />
</exec>
</tool>
Python Testing Integration
Integrate PDB debugging with Python testing frameworks to automatically trigger debugging sessions on test failures, debug complex test scenarios, and inspect test data during execution.
Basic Test Debugging Setup
# Set up PDB for test debugging
import os
os.environ['PYTHONBREAKPOINT'] = 'pdb.set_trace'
def test_with_debugging():
"""Example test with debugging capability"""
data = [1, 2, 3, 4, 5]
# Add breakpoint to inspect test data
breakpoint()
result = sum(data) / len(data)
assert result == 3.0
Testing Integration
Integrate PDB with testing frameworks to automatically trigger debugging sessions on test failures, debug complex test scenarios, and inspect test data during execution. This integration helps identify test failures and validate test logic interactively.
pytest Integration
pytest offers excellent PDB integration through fixtures, hooks, and command-line options. This setup enables automatic debugging on test failures, conditional debugging based on markers, and custom debugging workflows tailored to your testing needs.
conftest.py Setup
# conftest.py
import pytest
import os
import pdb
@pytest.fixture(autouse=True)
def debug_on_failure(request):
"""Automatically start debugger on test failure"""
yield
if request.node.rep_call.failed and os.getenv('PYTEST_DEBUG'):
pdb.post_mortem()
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Store test results for debug_on_failure fixture"""
outcome = yield
rep = outcome.get_result()
setattr(item, "rep_" + rep.when, rep)
@pytest.fixture
def debug_client():
"""Test client with debugging enabled"""
from myapp import create_app
app = create_app(testing=True, debug=True)
with app.test_client() as client:
with app.app_context():
yield client
# Custom pytest markers for debugging
def pytest_configure(config):
config.addinivalue_line(
"markers", "debug: mark test to run only when debugging is enabled"
)
def pytest_collection_modifyitems(config, items):
if not os.getenv('PYTEST_DEBUG'):
skip_debug = pytest.mark.skip(reason="Debug tests only run when PYTEST_DEBUG is set")
for item in items:
if "debug" in item.keywords:
item.add_marker(skip_debug)
Test Files with Debugging
# test_api.py
import pytest
import pdb
class TestAPI:
@pytest.mark.debug
def test_complex_calculation(self, debug_client):
"""Test with debugging enabled"""
pdb.set_trace()
# Complex test logic
test_data = {
'values': [1, 2, 3, 4, 5],
'operation': 'multiply',
'factor': 2
}
response = debug_client.post('/api/calculate', json=test_data)
# Debug response
pdb.set_trace()
assert response.status_code == 200
result = response.get_json()
assert result['success'] is True
def test_error_handling(self, debug_client):
"""Test error handling with conditional debugging"""
test_data = {'invalid': 'data'}
response = debug_client.post('/api/calculate', json=test_data)
# Debug only on unexpected results
if response.status_code != 400:
pdb.set_trace()
assert response.status_code == 400
# Run tests with debugging:
# PYTEST_DEBUG=1 python -m pytest test_api.py -v -s
unittest Integration
The unittest framework can be enhanced with custom debugging helpers and conditional breakpoints. This integration provides structured debugging support within Python's standard testing framework, enabling systematic debugging of test methods and assertions.
Debugging Test Cases
# test_example.py
import unittest
import os
import pdb
class DebugTestCase(unittest.TestCase):
def setUp(self):
"""Set up debugging context"""
self.debug_enabled = os.getenv('DEBUG_TESTS', False)
def debug_break(self, condition=True):
"""Conditional debugging helper"""
if self.debug_enabled and condition:
pdb.set_trace()
def test_calculation(self):
"""Test with debugging support"""
values = [1, 2, 3, 4, 5]
self.debug_break() # Start debugging if enabled
result = sum(values) / len(values)
# Debug unexpected results
self.debug_break(result != 3.0)
self.assertEqual(result, 3.0)
def test_edge_cases(self):
"""Test edge cases with conditional debugging"""
test_cases = [[], [0], [-1, 1], [float('inf')]]
for i, values in enumerate(test_cases):
with self.subTest(values=values):
# Debug specific test cases
self.debug_break(i == 2) # Debug third test case
if not values:
continue
for value in values:
self.debug_break(value < 0) # Debug negative values
self.assertGreaterEqual(value, 0)
if __name__ == '__main__':
# Enable debugging for unittest
os.environ['DEBUG_TESTS'] = '1'
unittest.main()
Docker Integration
Configure Docker containers to support interactive PDB debugging sessions. This integration enables debugging of containerized applications while maintaining container isolation and providing the necessary TTY and stdin access for PDB's interactive nature.
Dockerfile for Debugging
Create specialized Dockerfiles for debugging environments that include debugging tools, proper environment variables, and configurations optimized for development and troubleshooting scenarios.
# Dockerfile.debug
FROM python:3.9-slim
# Install debugging tools
RUN pip install pdb ipdb
# Set up working directory
WORKDIR /app
# Copy requirements and install dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
# Copy application code
COPY . .
# Set environment variables for debugging
ENV PYTHONUNBUFFERED=1
ENV PYTHONBREAKPOINT=pdb.set_trace
# Command for debugging
CMD ["python", "-u", "app.py"]
Docker Compose for Debugging
Docker Compose configurations for debugging require specific settings for TTY access, stdin support, and volume mounting to enable interactive debugging sessions while maintaining service orchestration.
# docker-compose.debug.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.debug
ports:
- "8000:8000"
volumes:
- .:/app
- /app/__pycache__
environment:
- DEBUG=1
- PYTHONBREAKPOINT=pdb.set_trace
tty: true
stdin_open: true
db:
image: postgres:13
environment:
POSTGRES_DB: debugapp
POSTGRES_USER: debug
POSTGRES_PASSWORD: debug
ports:
- "5432:5432"
Docker Debug Commands
Use these Docker commands to build, run, and connect to containers configured for PDB debugging. These commands provide various approaches for interactive debugging in containerized environments.
# Build and run with debugging
docker-compose -f docker-compose.debug.yml up --build
# Connect to running container for interactive debugging
docker exec -it container_name python -c "import pdb; pdb.set_trace()"
# Run specific script with debugging
docker run -it --rm app_image python -m pdb script.py
CI/CD Integration
Integrate debugging capabilities into continuous integration and deployment pipelines for troubleshooting test failures and deployment issues. This setup enables controlled debugging sessions in CI environments while maintaining security and workflow integrity.
GitHub Actions with Debugging
GitHub Actions workflows can be configured to support debugging through environment variables, conditional debugging sessions, and remote access tools. This integration helps troubleshoot pipeline failures and validate deployment processes.
# .github/workflows/debug-test.yml
name: Debug Tests
on:
workflow_dispatch:
inputs:
enable_debug:
description: 'Enable debugging session'
required: false
default: 'false'
type: choice
options:
- 'true'
- 'false'
jobs:
debug-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install debugging dependencies
run: |
pip install pdb pytest
pip install -r requirements.txt
- name: Run tests with debugging
env:
PYTHONBREAKPOINT: pdb.set_trace
DEBUG_TESTS: ${{ github.event.inputs.enable_debug || 'false' }}
run: |
if [ "$DEBUG_TESTS" = "true" ]; then
echo "Running tests with debugging enabled"
pytest -v -s --tb=short
else
echo "Running normal tests"
pytest -v --tb=short
fi
- name: Setup tmate session for debugging
if: ${{ failure() && github.event.inputs.enable_debug == 'true' }}
uses: mxschmitt/action-tmate@v3
with:
limit-access-to-actor: true
Best Practices for Integration
Environment-Based Configuration
# config.py
import os
class DebugConfig:
"""Centralized debug configuration"""
def __init__(self):
self.enabled = os.getenv('DEBUG', '0') == '1'
self.pdb_on_error = os.getenv('PDB_ON_ERROR', '0') == '1'
self.test_debug = os.getenv('TEST_DEBUG', '0') == '1'
def breakpoint(self, condition=True):
"""Conditional breakpoint"""
if self.enabled and condition:
import pdb; pdb.set_trace()
def post_mortem_wrapper(self, func):
"""Decorator for automatic post-mortem debugging"""
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception:
if self.pdb_on_error:
import pdb; pdb.post_mortem()
raise
return wrapper
# Global debug instance
debug = DebugConfig()
Integration Checklist
- IDE Setup: Configure launch configurations and tasks
- Framework Integration: Add debugging middleware and helpers
- Testing Integration: Set up automatic debugging on failures
- Docker Support: Configure containers for interactive debugging
- CI/CD Integration: Add debugging workflows for troubleshooting
- Script Automation: Create helper scripts for common debugging tasks
- Environment Configuration: Use environment variables for debug control
- Documentation: Document debugging procedures for team members
Further Reading
Official Documentation
- Official Python PDB Documentation
- Python Debugging With Pdb (Real Python)
- PDB Command Reference (Python Wiki)
- Debugging in VS Code (Python Docs)
- PyCharm Debugger Guide (JetBrains)
Tutorials and Guides
- Real Python PDB Guide - Comprehensive PDB tutorial
- Python Debugging with PDB - Python Module of the Week guide
IDE-Specific Resources
- VS Code Python Debugging - VS Code debugging guide
- PyCharm Debugging - PyCharm debugging reference
Testing Integration Resources
- pytest Documentation - pytest testing framework
- Python unittest Documentation - Built-in testing framework
PDB is a foundational tool for Python developers—mastering it will make you faster at diagnosing and fixing bugs in any environment.