Debugging Techniques
Learning Objectives
- By the end of this lesson, you will be able to:
- - Use print statements for debugging
- - Use the Python debugger (pdb) for interactive debugging
- - Understand logging basics and the logging module
- - Set breakpoints and step through code
- - Inspect variables and call stacks
- - Use different logging levels
- - Configure loggers and handlers
- - Apply debugging techniques effectively
- - Choose appropriate debugging methods
Lesson 10.4: Debugging Techniques
Learning Objectives
By the end of this lesson, you will be able to:
- Use print statements for debugging
- Use the Python debugger (pdb) for interactive debugging
- Understand logging basics and the logging module
- Set breakpoints and step through code
- Inspect variables and call stacks
- Use different logging levels
- Configure loggers and handlers
- Apply debugging techniques effectively
- Choose appropriate debugging methods
Introduction to Debugging
Debugging is the process of finding and fixing errors (bugs) in your code. Python provides several tools and techniques for debugging.
Why Debugging Matters
- Find errors: Locate bugs in code
- Understand behavior: See how code executes
- Inspect state: Check variable values
- Trace execution: Follow program flow
Debugging Approaches
- Print debugging: Simple but effective
- Debugger (pdb): Interactive debugging
- Logging: Structured debugging information
- IDE debuggers: Visual debugging tools
Print Debugging
Print debugging is the simplest debugging technique - adding print statements to see what's happening.
Basic Print Debugging
def calculate_total(items):
print(f"DEBUG: calculate_total called with {items}")
total = 0
for item in items:
print(f"DEBUG: Processing item {item}")
total += item
print(f"DEBUG: Total is now {total}")
print(f"DEBUG: Final total: {total}")
return total
calculate_total([10, 20, 30])
Debugging with f-strings
def divide(a, b):
print(f"DEBUG: divide({a}, {b})")
result = a / b
print(f"DEBUG: result = {result}")
return result
Conditional Debugging
DEBUG = True
def process_data(data):
if DEBUG:
print(f"DEBUG: Processing data: {data}")
result = data * 2
if DEBUG:
print(f"DEBUG: Result: {result}")
return result
Debugging with Separators
def complex_function(x, y):
print("=" * 40)
print(f"DEBUG: Entering function with x={x}, y={y}")
result = x + y
print(f"DEBUG: Calculation result: {result}")
print("=" * 40)
return result
Python Debugger (pdb)
The Python debugger (pdb) is an interactive debugger for Python programs.
Starting the Debugger
Method 1: pdb.set_trace()
import pdb
def calculate(a, b):
pdb.set_trace() # Breakpoint here
result = a + b
return result
calculate(5, 10)
Method 2: Command Line
python -m pdb script.py
Method 3: Post-Mortem Debugging
import pdb
def buggy_function():
x = 1
y = 0
result = x / y # Error here
try:
buggy_function()
except:
pdb.post_mortem() # Enter debugger on exception
Basic pdb Commands
| Command | Shortcut | Description |
|---|---|---|
help |
h |
Show help |
next |
n |
Execute next line |
step |
s |
Step into function |
continue |
c |
Continue execution |
list |
l |
Show current code |
print |
p |
Print variable |
pp |
pp |
Pretty print |
where |
w |
Show stack trace |
break |
b |
Set breakpoint |
quit |
q |
Quit debugger |
Using pdb Commands
import pdb
def process_numbers(numbers):
pdb.set_trace()
total = 0
for num in numbers:
total += num
return total
process_numbers([1, 2, 3, 4, 5])
# In debugger:
# (Pdb) n # Next line
# (Pdb) p total # Print total
# (Pdb) p numbers # Print numbers
# (Pdb) l # List code
# (Pdb) c # Continue
Stepping Through Code
import pdb
def add(a, b):
result = a + b
return result
def calculate(x, y):
pdb.set_trace()
sum_result = add(x, y)
product = x * y
return sum_result + product
calculate(5, 3)
# (Pdb) n # Step over add()
# (Pdb) s # Step into add()
# (Pdb) n # Next line in add()
# (Pdb) c # Continue
Inspecting Variables
import pdb
def process_data(data):
pdb.set_trace()
filtered = [x for x in data if x > 0]
total = sum(filtered)
average = total / len(filtered)
return average
process_data([1, -2, 3, -4, 5])
# (Pdb) p data # Print data
# (Pdb) p filtered # Print filtered
# (Pdb) pp data # Pretty print data
# (Pdb) type(data) # Check type
Setting Breakpoints
import pdb
def function_a():
x = 1
y = 2
return x + y
def function_b():
result = function_a()
pdb.set_trace() # Breakpoint
return result * 2
function_b()
# (Pdb) b function_a # Set breakpoint in function_a
# (Pdb) b 5 # Set breakpoint at line 5
# (Pdb) c # Continue to breakpoint
Logging Basics
The logging module provides a flexible framework for emitting log messages.
Basic Logging
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug("Debug message")
logging.info("Info message")
logging.warning("Warning message")
logging.error("Error message")
logging.critical("Critical message")
Logging Levels
| Level | Numeric Value | Description |
|---|---|---|
| DEBUG | 10 | Detailed debugging information |
| INFO | 20 | Informational messages |
| WARNING | 30 | Warning messages |
| ERROR | 40 | Error messages |
| CRITICAL | 50 | Critical error messages |
Configuring Logging
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logging.info("Application started")
logging.error("An error occurred")
Logging to File
import logging
logging.basicConfig(
filename='app.log',
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logging.debug("Debug message")
logging.info("Info message")
logging.error("Error message")
Using Loggers
import logging
# Create logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Create handler
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
# Create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
# Add handler to logger
logger.addHandler(handler)
logger.debug("Debug message")
logger.info("Info message")
logger.error("Error message")
Logging in Functions
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def process_data(data):
logger.debug(f"Processing data: {data}")
try:
result = sum(data)
logger.info(f"Processed successfully: {result}")
return result
except Exception as e:
logger.error(f"Error processing data: {e}")
raise
process_data([1, 2, 3, 4, 5])
Logging Best Practices
import logging
# Use module-level logger
logger = logging.getLogger(__name__)
def function_with_logging():
logger.debug("Entering function")
try:
# Code here
logger.info("Operation successful")
except Exception as e:
logger.error(f"Operation failed: {e}", exc_info=True)
raise
finally:
logger.debug("Exiting function")
Practical Examples
Example 1: Debugging with Print
def find_max(numbers):
print(f"DEBUG: Input: {numbers}")
if not numbers:
print("DEBUG: Empty list")
return None
max_val = numbers[0]
print(f"DEBUG: Initial max: {max_val}")
for num in numbers[1:]:
print(f"DEBUG: Checking {num} against {max_val}")
if num > max_val:
max_val = num
print(f"DEBUG: New max: {max_val}")
print(f"DEBUG: Final max: {max_val}")
return max_val
find_max([3, 1, 4, 1, 5, 9, 2, 6])
Example 2: Debugging with pdb
import pdb
def binary_search(arr, target):
pdb.set_trace()
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
result = binary_search([1, 2, 3, 4, 5], 3)
Example 3: Logging in Application
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def process_user(user_id):
logger.info(f"Processing user {user_id}")
try:
# Process user
logger.debug(f"User {user_id} processed successfully")
return True
except Exception as e:
logger.error(f"Error processing user {user_id}: {e}")
return False
process_user(123)
Example 4: Conditional Debugging
import logging
import os
# Set debug level from environment
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO
logging.basicConfig(level=LOG_LEVEL)
logger = logging.getLogger(__name__)
def complex_operation(data):
logger.debug(f"Starting operation with data: {data}")
# Complex processing
result = data * 2
logger.info(f"Operation completed: {result}")
return result
Common Debugging Patterns
Pattern 1: Debug Flag
DEBUG = True
def function():
if DEBUG:
print("Debug: Function called")
# Code here
Pattern 2: Logging Decorator
import logging
from functools import wraps
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def log_function(func):
@wraps(func)
def wrapper(*args, **kwargs):
logger.debug(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
logger.debug(f"{func.__name__} returned {result}")
return result
return wrapper
@log_function
def add(a, b):
return a + b
add(5, 3)
Pattern 3: Assertions for Debugging
def process_data(data):
assert isinstance(data, list), "Data must be a list"
assert len(data) > 0, "Data cannot be empty"
# Process data
return sum(data)
Best Practices
1. Use Appropriate Debugging Method
- Print: Quick debugging, simple issues
- pdb: Complex debugging, interactive inspection
- Logging: Production code, persistent debugging
2. Remove Debug Code
# Good: Use logging instead of print
import logging
logger = logging.getLogger(__name__)
logger.debug("Debug message")
# Avoid: Leaving print statements
print("DEBUG: message") # Should be removed
3. Use Logging Levels Appropriately
logger.debug("Detailed debugging info")
logger.info("General information")
logger.warning("Warning messages")
logger.error("Error messages")
logger.critical("Critical errors")
4. Don't Over-Debug
# Bad: Too many debug statements
def function():
print("DEBUG: Start")
print("DEBUG: Step 1")
print("DEBUG: Step 2")
print("DEBUG: Step 3")
print("DEBUG: End")
# Good: Strategic debugging
def function():
logger.debug("Function started")
# Key operations
logger.debug("Function completed")
Common Mistakes and Pitfalls
1. Leaving Print Statements
# WRONG: Print statements in production code
def function():
print("DEBUG: Processing")
# Code
print("DEBUG: Done")
# BETTER: Use logging
def function():
logger.debug("Processing")
# Code
logger.debug("Done")
2. Not Using Appropriate Log Levels
# WRONG: Using error for everything
logging.error("User logged in")
logging.error("Data processed")
# BETTER: Use appropriate levels
logging.info("User logged in")
logging.debug("Data processed")
3. Too Much Debugging
# WRONG: Debugging every line
print(f"x = {x}")
x = x + 1
print(f"x = {x}")
x = x * 2
print(f"x = {x}")
# BETTER: Debug key points
logger.debug(f"Initial value: {x}")
# Process
logger.debug(f"Final value: {x}")
Practice Exercise
Exercise: Debugging Practice
Objective: Create a Python program that demonstrates various debugging techniques.
Instructions:
-
Create a file called
debugging_practice.py -
Write a program that:
- Uses print debugging
- Demonstrates pdb usage
- Uses logging
- Shows debugging patterns
-
Your program should include:
- Print debugging examples
- pdb debugging examples
- Logging examples
- Practical debugging scenarios
Example Solution:
"""
Debugging Practice
This program demonstrates various debugging techniques.
"""
import pdb
import logging
print("=" * 60)
print("DEBUGGING PRACTICE")
print("=" * 60)
print()
# 1. Basic print debugging
print("1. BASIC PRINT DEBUGGING")
print("-" * 60)
def calculate_sum(numbers):
print(f"DEBUG: Input numbers: {numbers}")
total = 0
for num in numbers:
print(f"DEBUG: Adding {num} to total (current: {total})")
total += num
print(f"DEBUG: Final total: {total}")
return total
result = calculate_sum([1, 2, 3, 4, 5])
print(f"Result: {result}\n")
# 2. Conditional debugging
print("2. CONDITIONAL DEBUGGING")
print("-" * 60)
DEBUG = True
def process_data(data):
if DEBUG:
print(f"DEBUG: Processing data: {data}")
result = data * 2
if DEBUG:
print(f"DEBUG: Result: {result}")
return result
result = process_data(10)
print(f"Result: {result}\n")
# 3. Debugging with separators
print("3. DEBUGGING WITH SEPARATORS")
print("-" * 60)
def complex_function(x, y):
print("=" * 40)
print(f"DEBUG: Entering function with x={x}, y={y}")
intermediate = x * y
print(f"DEBUG: Intermediate result: {intermediate}")
result = intermediate + x + y
print(f"DEBUG: Final result: {result}")
print("=" * 40)
return result
result = complex_function(5, 3)
print(f"Result: {result}\n")
# 4. Basic logging
print("4. BASIC LOGGING")
print("-" * 60)
logging.basicConfig(level=logging.DEBUG)
logging.debug("Debug message")
logging.info("Info message")
logging.warning("Warning message")
logging.error("Error message")
logging.critical("Critical message")
print()
# 5. Logging with configuration
print("5. LOGGING WITH CONFIGURATION")
print("-" * 60)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%H:%M:%S'
)
logging.info("Application started")
logging.warning("This is a warning")
logging.error("An error occurred")
print()
# 6. Logging to file
print("6. LOGGING TO FILE")
print("-" * 60)
logging.basicConfig(
filename='debug.log',
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logging.debug("Debug message to file")
logging.info("Info message to file")
logging.error("Error message to file")
print("Logged to debug.log\n")
# 7. Using logger object
print("7. USING LOGGER OBJECT")
print("-" * 60)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# Create console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# Create formatter
formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.debug("Debug message (won't show)")
logger.info("Info message")
logger.error("Error message")
print()
# 8. Logging in function
print("8. LOGGING IN FUNCTION")
print("-" * 60)
def process_items(items):
logger.info(f"Processing {len(items)} items")
try:
total = sum(items)
logger.info(f"Successfully processed: total={total}")
return total
except Exception as e:
logger.error(f"Error processing items: {e}")
raise
try:
result = process_items([1, 2, 3, 4, 5])
print(f"Result: {result}\n")
except Exception:
pass
# 9. Debugging with assertions
print("9. DEBUGGING WITH ASSERTIONS")
print("-" * 60)
def divide(a, b):
assert b != 0, "Division by zero"
assert isinstance(a, (int, float)), "First argument must be number"
assert isinstance(b, (int, float)), "Second argument must be number"
return a / b
try:
result = divide(10, 2)
print(f"10 / 2 = {result}")
# Uncomment to test assertions:
# divide(10, 0) # AssertionError
except AssertionError as e:
print(f"Assertion failed: {e}")
print()
# 10. Logging exception information
print("10. LOGGING EXCEPTION INFORMATION")
print("-" * 60)
def risky_operation():
try:
result = 10 / 0
return result
except Exception as e:
logger.error(f"Error in risky_operation: {e}", exc_info=True)
raise
try:
risky_operation()
except Exception:
pass
print()
# 11. Debugging decorator pattern
print("11. DEBUGGING DECORATOR PATTERN")
print("-" * 60)
from functools import wraps
def debug_function(func):
@wraps(func)
def wrapper(*args, **kwargs):
logger.debug(f"Calling {func.__name__}(*{args}, **{kwargs})")
try:
result = func(*args, **kwargs)
logger.debug(f"{func.__name__} returned {result}")
return result
except Exception as e:
logger.error(f"Error in {func.__name__}: {e}")
raise
return wrapper
@debug_function
def multiply(a, b):
return a * b
result = multiply(5, 3)
print(f"5 * 3 = {result}\n")
# 12. Conditional logging
print("12. CONDITIONAL LOGGING")
print("-" * 60)
import os
# Set log level from environment or default
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
logging.basicConfig(level=getattr(logging, LOG_LEVEL))
logger = logging.getLogger(__name__)
logger.debug("Debug message (may not show)")
logger.info("Info message")
logger.warning("Warning message")
print()
# 13. Multiple handlers
print("13. MULTIPLE HANDLERS")
print("-" * 60)
logger = logging.getLogger("multi_handler")
logger.setLevel(logging.DEBUG)
# Console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# File handler
file_handler = logging.FileHandler("detailed.log")
file_handler.setLevel(logging.DEBUG)
# Formatter
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.debug("Debug message (file only)")
logger.info("Info message (both)")
logger.error("Error message (both)")
print("Check detailed.log for all messages\n")
# 14. Finding bugs with debugging
print("14. FINDING BUGS WITH DEBUGGING")
print("-" * 60)
def buggy_function(numbers):
# Bug: doesn't handle empty list
total = 0
for i in range(len(numbers)):
total += numbers[i] # Should use sum()
average = total / len(numbers) # Bug: division by zero if empty
return average
# Debug version
def debugged_function(numbers):
logger.debug(f"Input: {numbers}")
if not numbers:
logger.warning("Empty list provided")
return None
total = sum(numbers) # Better approach
logger.debug(f"Total: {total}, Count: {len(numbers)}")
average = total / len(numbers)
logger.info(f"Average: {average}")
return average
result = debugged_function([1, 2, 3, 4, 5])
print(f"Average: {result}\n")
# 15. pdb example (commented - uncomment to try)
print("15. PDB EXAMPLE")
print("-" * 60)
print("Uncomment the code below to try pdb:")
print()
print("""
def example_with_pdb():
import pdb; pdb.set_trace()
x = 10
y = 20
result = x + y
return result
# Uncomment to try:
# result = example_with_pdb()
""")
print()
# Cleanup
import os
if os.path.exists('debug.log'):
os.remove('debug.log')
if os.path.exists('detailed.log'):
os.remove('detailed.log')
print("=" * 60)
print("PRACTICE COMPLETE!")
print("=" * 60)
Expected Output (truncated):
============================================================
DEBUGGING PRACTICE
============================================================
1. BASIC PRINT DEBUGGING
------------------------------------------------------------
DEBUG: Input numbers: [1, 2, 3, 4, 5]
DEBUG: Adding 1 to total (current: 0)
DEBUG: Adding 2 to total (current: 1)
[... rest of output ...]
Challenge (Optional):
- Create a debugging utility module
- Build a logging configuration system
- Create a debug decorator library
- Implement a trace logging system
Key Takeaways
- Print debugging is simple but effective for quick debugging
- pdb is Python's interactive debugger
- Logging module provides structured debugging information
- Logging levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
- pdb commands: n (next), s (step), c (continue), p (print), l (list)
- Use logging instead of print for production code
- Set appropriate log levels for different environments
- Loggers can have multiple handlers (console, file, etc.)
- Format logging messages for better readability
- Use exc_info=True to log exception tracebacks
- Remove debug code before production (or use logging)
- Use assertions for debugging assumptions
- Conditional debugging with flags or environment variables
- Don't over-debug - be strategic about what to log
- Choose appropriate method for your debugging needs
Quiz: Debugging
Test your understanding with these questions:
-
What is the simplest debugging technique?
- A) pdb
- B) Print debugging
- C) Logging
- D) IDE debugger
-
What command starts the Python debugger?
- A)
pdb.start() - B)
pdb.set_trace() - C)
pdb.debug() - D)
pdb.break()
- A)
-
What pdb command executes the next line?
- A)
s - B)
n - C)
c - D)
p
- A)
-
What is the default logging level?
- A) DEBUG
- B) INFO
- C) WARNING
- D) ERROR
-
What logging level shows detailed debugging info?
- A) DEBUG
- B) INFO
- C) WARNING
- D) ERROR
-
What should you use instead of print in production?
- A) pdb
- B) Logging
- C) assert
- D) input()
-
What pdb command steps into a function?
- A)
n - B)
s - C)
c - D)
l
- A)
-
What logging level is for warnings?
- A) DEBUG
- B) INFO
- C) WARNING
- D) ERROR
-
What pdb command shows the current code?
- A)
p - B)
l - C)
w - D)
c
- A)
-
What should you do with debug print statements?
- A) Leave them
- B) Remove or replace with logging
- C) Comment them out
- D) Move them to separate file
Answers:
- B) Print debugging (simplest technique)
- B)
pdb.set_trace()(starts debugger at that point) - B)
n(next - executes next line) - C) WARNING (default logging level)
- A) DEBUG (shows detailed debugging info)
- B) Logging (should use logging in production)
- B)
s(step - steps into function) - C) WARNING (logging level for warnings)
- B)
l(list - shows current code) - B) Remove or replace with logging (clean up debug code)
Next Steps
Excellent work! You've mastered debugging techniques. You now understand:
- How to use print debugging
- How to use the pdb debugger
- How to use logging
- When to use each technique
What's Next?
- Module 11: Modules and Packages
- Learn about importing modules
- Understand package structure
- Explore Python's module system
Additional Resources
- Logging: docs.python.org/3/library/logging.html
- pdb: docs.python.org/3/library/pdb.html
- Debugging: docs.python.org/3/library/pdb.html#debugger-commands
Lesson completed! You're ready to move on to the next module.
Course Navigation
- Exceptions
- Try-Except Blocks
- Raising Exceptions
- Debugging Techniques