Python Debugging Tips: From Print to Advanced Debuggers
Debugging is a critical skill. Here are the most effective techniques to find and fix bugs in Python:
1. Strategic Print Debugging
# Basic debugging
def calculate_total(items):
print(f"DEBUG: items = {items}") # See input
total = sum(item['price'] for item in items)
print(f"DEBUG: total = {total}") # See result
return total
# Better: Print with context
print(f"DEBUG [{__name__}:{42}]: variable = {value}")
# Print type and value
print(f"value = {value} (type: {type(value)})")
# Pretty print complex objects
import pprint
pprint.pprint(complex_data)
2. Using assert for Assumptions
# Verify assumptions during development
def process_user(user_id):
user = get_user(user_id)
assert user is not None, f"User {user_id} not found"
assert isinstance(user.age, int), f"Age should be int, got {type(user.age)}"
assert user.age >= 0, f"Invalid age: {user.age}"
return user
# Assertions are removed in optimized mode (-O flag)
# Don't use for runtime validation in production!
3. Python Debugger (pdb)
# Set breakpoint
import pdb
def buggy_function(data):
result = process(data)
pdb.set_trace() # Execution pauses here
return result
# Python 3.7+: built-in breakpoint()
def buggy_function(data):
result = process(data)
breakpoint() # Better than pdb.set_trace()
return result
# Common pdb commands:
# n (next) - Execute next line
# s (step) - Step into function
# c (continue) - Continue execution
# l (list) - Show code context
# p variable - Print variable
# pp variable - Pretty-print variable
# w (where) - Show stack trace
# q (quit) - Exit debugger
4. Interactive Debugging Session
# Start debugger on error
python -m pdb script.py
# Post-mortem debugging
import pdb
try:
risky_operation()
except Exception:
pdb.post_mortem() # Debug from point of failure
# Inspect variables in pdb
(Pdb) p variable_name
(Pdb) pp complex_dict
(Pdb) type(my_var)
(Pdb) dir(object) # See all attributes
5. Logging Instead of Print
import logging
# Configure logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
filename='debug.log'
)
logger = logging.getLogger(__name__)
# Use different levels
logger.debug("Detailed debug information")
logger.info("General information")
logger.warning("Warning message")
logger.error("Error occurred")
logger.exception("Error with stack trace") # Use in except blocks
# Example
def process_data(data):
logger.debug(f"Processing {len(data)} items")
try:
result = transform(data)
logger.info(f"Successfully processed data")
return result
except Exception as e:
logger.exception(f"Failed to process data")
raise
6. Debugging Techniques
# Binary search debugging
# Comment out half the code to isolate the bug
# Rubber duck debugging
# Explain code line-by-line to find the issue
# Diff debugging
# Compare working vs broken versions
# Check assumptions
def divide(a, b):
print(f"Dividing {a} by {b}")
print(f"Types: {type(a)}, {type(b)}")
assert b != 0, "Division by zero"
return a / b
# Simplify the problem
# Create minimal reproducible example
7. Inspect Variables and Objects
# Check what's inside an object
import inspect
# List all attributes
print(dir(object))
# Get signature of function
print(inspect.signature(function))
# Check if callable
if callable(my_var):
my_var()
# View source code
print(inspect.getsource(function))
# Check variable type and value
def debug_var(var):
print(f"Type: {type(var)}")
print(f"Value: {var}")
print(f"Dir: {dir(var)}")
if hasattr(var, '__dict__'):
print(f"Attributes: {var.__dict__}")
8. Common Debugging Patterns
# Debug decorator
def debug(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
print(f"Args: {args}, Kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"Result: {result}")
return result
return wrapper
@debug
def calculate(x, y):
return x + y
# Conditional breakpoint
if suspicious_condition:
breakpoint()
# Time execution
import time
start = time.time()
slow_function()
print(f"Took {time.time() - start:.2f} seconds")
# Memory debugging
import sys
print(f"Size: {sys.getsizeof(object)} bytes")
9. IDE Debuggers
# VS Code
# - Click left of line number to set breakpoint
# - F5 to start debugging
# - F10 step over, F11 step into
# - Hover over variables to inspect
# PyCharm
# - Similar breakpoint system
# - More advanced variable inspection
# - Conditional breakpoints
# - Evaluate expression while debugging
# Jupyter/IPython
%debug # Enter debugger after exception
%pdb on # Auto-start debugger on exception
10. Advanced Tools
# ipdb (enhanced pdb with IPython features)
pip install ipdb
import ipdb; ipdb.set_trace()
# pudb (visual debugger in terminal)
pip install pudb
import pudb; pudb.set_trace()
# Better exceptions
pip install rich
from rich.traceback import install
install(show_locals=True)
# Memory profiler
from memory_profiler import profile
@profile
def memory_heavy_function():
pass
Best Practices
- ✅ Use
logginginstead ofprintfor production - ✅ Use
breakpoint()overpdb.set_trace() - ✅ Remove debug code before committing
- ✅ Learn your IDE's debugger (faster than print)
- ✅ Write tests to prevent regressions
- ❌ Don't leave print() statements in production
- ❌ Don't use
assertfor runtime validation - ⚠️ Use different log levels appropriately
Pro Tip: When debugging complex issues, use binary search: comment out half the code to see if the bug persists. Repeat until you isolate the problem. This is often faster than stepping through every line.
← Back to Python Tips