Skip to main content

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 modern breakpoint()
  • 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 ll to see full function context
  • Combine w and l to understand location
  • Use pp for complex data structures
  • Set conditional breakpoints to avoid noise
  • Use interact for 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

  1. Use environment variables for debug control
  2. Create aliases for frequently used commands
  3. Separate configuration by environment (dev/staging/prod)
  4. Document custom commands in your .pdbrc file
  5. Version control your .pdbrc for team sharing
  6. 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

  1. IDE Setup: Configure launch configurations and tasks
  2. Framework Integration: Add debugging middleware and helpers
  3. Testing Integration: Set up automatic debugging on failures
  4. Docker Support: Configure containers for interactive debugging
  5. CI/CD Integration: Add debugging workflows for troubleshooting
  6. Script Automation: Create helper scripts for common debugging tasks
  7. Environment Configuration: Use environment variables for debug control
  8. Documentation: Document debugging procedures for team members

Further Reading

Official Documentation

Tutorials and Guides

IDE-Specific Resources

Testing Integration Resources


PDB is a foundational tool for Python developers—mastering it will make you faster at diagnosing and fixing bugs in any environment.