Useful Data Tips

Python Debugging Tips: From Print to Advanced Debuggers

⏱️ 35 sec read 🐍 Python

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

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