4.5 Debugging Tools and Techniques¶
What is Debugging?¶
Debugging is the process of finding and fixing errors (called “bugs”) in your code. Even experienced programmers write code that doesn’t work perfectly the first time, so debugging is an essential skill for every programmer.
Think of debugging like being a detective - you need to gather clues, form hypotheses about what’s wrong, test your theories, and systematically eliminate possibilities until you find the culprit.
Types of Bugs¶
Before we learn debugging techniques, let’s understand the different types of bugs you’ll encounter:
Syntax Errors¶
Code that doesn’t follow Python’s rules and won’t run at all.
# Syntax errors - these won't run
def calculate_average(numbers: # Missing closing parenthesis
return sum(numbers) / len(numbers)
print("Hello World" # Missing closing quote and parenthesis
Runtime Errors¶
Code that runs but crashes during execution.
# Runtime error - division by zero
def calculate_average(numbers):
return sum(numbers) / len(numbers)
# This will crash when called with an empty list
result = calculate_average([]) # ZeroDivisionError
Logic Errors¶
Code that runs without crashing but produces incorrect results.
# Logic error - off-by-one error
def get_last_element(my_list):
return my_list[len(my_list)] # Should be len(my_list) - 1
# This will cause an IndexError, or if we "fix" it incorrectly:
def get_last_element_wrong(my_list):
if len(my_list) > 0:
return my_list[len(my_list) - 2] # Returns second-to-last, not last!
return None
Debugging Output Statements¶
Basic Print Debugging¶
The simplest print() statements to see what your code is doing.debugging technique is adding
def calculate_grade_average(assignments, tests, final_exam):
"""Calculate student's final grade with weighted averages."""
# Add debug prints to trace execution
print(f"DEBUG: Input assignments: {assignments}")
print(f"DEBUG: Input tests: {tests}")
print(f"DEBUG: Input final_exam: {final_exam}")
# Calculate assignment average (30% of grade)
if assignments:
assignment_avg = sum(assignments) / len(assignments)
print(f"DEBUG: Assignment average: {assignment_avg}")
else:
assignment_avg = 0
print("DEBUG: No assignments provided")
# Calculate test average (40% of grade)
if tests:
test_avg = sum(tests) / len(tests)
print(f"DEBUG: Test average: {test_avg}")
else:
test_avg = 0
print("DEBUG: No tests provided")
# Final exam is 30% of grade
print(f"DEBUG: Final exam score: {final_exam}")
# Calculate weighted final grade
final_grade = (assignment_avg * 0.3) + (test_avg * 0.4) + (final_exam * 0.3)
print(f"DEBUG: Calculated final grade: {final_grade}")
return final_grade
# Test the function
assignments = [85, 90, 88]
tests = [92, 87]
final = 89
result = calculate_grade_average(assignments, tests, final)
print(f"Final result: {result}")
Strategic Print Placement¶
def find_maximum_value(numbers):
"""Find the maximum value in a list - with debugging."""
print(f"DEBUG: Starting with list: {numbers}")
if not numbers:
print("DEBUG: Empty list detected")
return None
max_value = numbers[0]
print(f"DEBUG: Initial max_value: {max_value}")
for i in range(1, len(numbers)):
print(f"DEBUG: Checking index {i}, value {numbers[i]}")
if numbers[i] > max_value:
print(f"DEBUG: Found new maximum: {numbers[i]} > {max_value}")
max_value = numbers[i]
else:
print(f"DEBUG: {numbers[i]} is not greater than current max {max_value}")
print(f"DEBUG: Final maximum: {max_value}")
return max_value
# Test with debug output
test_numbers = [3, 7, 2, 9, 1, 8]
maximum = find_maximum_value(test_numbers)
print(f"Maximum value found: {maximum}")
The Factorial Debugging Activity¶
Let’s work through debugging a factorial function that returns wrong results for input 0.
The Buggy Factorial Function¶
def factorial_buggy(n):
"""Calculate factorial of n - but has bugs!"""
result = 1
# Bug 1: Wrong condition for the loop
for i in range(n): # Should be range(1, n + 1)
result = result * i
return result
# Test the function
print("Testing factorial function:")
print(f"factorial(0) = {factorial_buggy(0)} (should be 1)")
print(f"factorial(1) = {factorial_buggy(1)} (should be 1)")
print(f"factorial(5) = {factorial_buggy(5)} (should be 120)")
Debugging with Print Statements¶
def factorial_debug_v1(n):
"""Debug the factorial function with print statements."""
print(f"DEBUG: Called factorial_debug_v1({n})")
result = 1
print(f"DEBUG: Initial result = {result}")
print(f"DEBUG: About to start loop with range({n})")
print(f"DEBUG: range({n}) produces: {list(range(n))}")
for i in range(n):
print(f"DEBUG: Loop iteration - i = {i}")
print(f"DEBUG: Before multiplication - result = {result}")
result = result * i
print(f"DEBUG: After multiplication - result = {result}")
print(f"DEBUG: Final result = {result}")
return result
# Test with debugging
print("=== Debugging factorial function ===")
print(f"factorial_debug_v1(0) = {factorial_debug_v1(0)}")
print(f"factorial_debug_v1(3) = {factorial_debug_v1(3)}")
Identifying and Fixing the Bug¶
def factorial_debug_v2(n):
"""Second attempt at fixing factorial - still debugging."""
print(f"DEBUG: Called factorial_debug_v2({n})")
# Special case for 0! = 1
if n == 0:
print("DEBUG: Special case n=0, returning 1")
return 1
result = 1
print(f"DEBUG: Initial result = {result}")
# Fix: Use range(1, n + 1) instead of range(n)
print(f"DEBUG: Using range(1, {n + 1}) = {list(range(1, n + 1))}")
for i in range(1, n + 1):
print(f"DEBUG: Loop iteration - i = {i}")
print(f"DEBUG: Before multiplication - result = {result}")
result = result * i
print(f"DEBUG: After multiplication - result = {result}")
print(f"DEBUG: Final result = {result}")
return result
# Test the fixed version
print("\n=== Testing fixed factorial function ===")
for test_value in [0, 1, 3, 5]:
result = factorial_debug_v2(test_value)
print(f"factorial({test_value}) = {result}")
print()
Clean Final Version¶
def factorial_clean(n):
"""Calculate factorial of n - clean final version."""
# Handle special case
if n == 0:
return 1
# Calculate factorial
result = 1
for i in range(1, n + 1):
result = result * i
return result
# Test clean version
print("=== Clean factorial function ===")
test_cases = [0, 1, 3, 5, 10]
expected = [1, 1, 6, 120, 3628800]
for i, test_val in enumerate(test_cases):
actual = factorial_clean(test_val)
expected_val = expected[i]
status = "✓ PASS" if actual == expected_val else "✗ FAIL"
print(f"factorial({test_val}) = {actual} (expected {expected_val}) {status}")
IDE Debugger Tools¶
Modern IDEs provide powerful debugging tools that go beyond print statements.
Setting Breakpoints¶
Breakpoints are markers that pause your program’s execution at specific lines, allowing you to inspect the program’s state.
def calculate_compound_interest(principal, rate, time):
"""Calculate compound interest - demonstrates breakpoint usage."""
# Set breakpoint here to inspect initial values
annual_rate = rate / 100
# Set breakpoint here to check the conversion
amount = principal
# Set breakpoint here to watch the loop
for year in range(time):
# Set breakpoint inside loop to see year-by-year changes
interest = amount * annual_rate
amount = amount + interest
# You can inspect 'year', 'interest', and 'amount' at each iteration
# Set breakpoint here to see final result
total_interest = amount - principal
return amount, total_interest
# When debugging, you can:
# 1. Set breakpoints by clicking in the margin (IDE-specific)
# 2. Run in debug mode
# 3. Inspect variables when execution pauses
# 4. Step through code line by line
Single Line Stepping¶
Single line stepping lets you execute your program one line at a time.
def bubble_sort_debug(arr):
"""Bubble sort with comments for stepping through."""
n = len(arr)
# Step here: Check initial array length
for i in range(n):
# Step here: Start of outer loop - check value of i
for j in range(0, n - i - 1):
# Step here: Start of inner loop - check values of i and j
if arr[j] > arr[j + 1]:
# Step here: Found elements to swap - inspect arr[j] and arr[j+1]
arr[j], arr[j + 1] = arr[j + 1], arr[j]
# Step here: After swap - see the array change
# Step here: Final sorted array
return arr
# When stepping through:
# 1. Use "Step Over" to execute the current line
# 2. Use "Step Into" to enter function calls
# 3. Use "Step Out" to exit the current function
# 4. Watch how variables change with each step
Watches and Variable Inspection¶
Watches let you monitor specific variables or expressions during debugging.
def analyze_student_grades(students_data):
"""Analyze grades - good for watching variables."""
total_students = len(students_data)
# Watch: total_students
total_score = 0
passing_students = 0
# Watch: total_score, passing_students
grade_distribution = {"A": 0, "B": 0, "C": 0, "D": 0, "F": 0}
# Watch: grade_distribution (expand to see all values)
for student in students_data:
score = student["score"]
total_score += score
# Watch: score, total_score (see running total)
if score >= 60:
passing_students += 1
# Watch: passing_students (see count increase)
# Determine letter grade
if score >= 90:
letter_grade = "A"
elif score >= 80:
letter_grade = "B"
elif score >= 70:
letter_grade = "C"
elif score >= 60:
letter_grade = "D"
else:
letter_grade = "F"
grade_distribution[letter_grade] += 1
# Watch: letter_grade, grade_distribution[letter_grade]
average_score = total_score / total_students
passing_rate = (passing_students / total_students) * 100
# Watch: average_score, passing_rate
return {
"average": average_score,
"passing_rate": passing_rate,
"distribution": grade_distribution
}
# Sample data for testing
sample_students = [
{"name": "Alice", "score": 92},
{"name": "Bob", "score": 87},
{"name": "Charlie", "score": 76},
{"name": "Diana", "score": 95},
{"name": "Eve", "score": 58}
]
results = analyze_student_grades(sample_students)
print("Grade analysis results:", results)
Debugging Function Interfaces¶
Understanding how functions interact is crucial for debugging larger programs.
Debugging Function Parameters¶
def calculate_shipping_cost(weight, distance, shipping_type):
"""Calculate shipping cost - debug parameter passing."""
print(f"DEBUG: Function called with:")
print(f" weight: {weight} (type: {type(weight)})")
print(f" distance: {distance} (type: {type(distance)})")
print(f" shipping_type: {shipping_type} (type: {type(shipping_type)})")
# Validate parameters
if weight <= 0:
print(f"DEBUG: Invalid weight: {weight}")
return None
if distance <= 0:
print(f"DEBUG: Invalid distance: {distance}")
return None
# Base rates
base_rates = {
"standard": 0.50,
"express": 1.00,
"overnight": 2.00
}
if shipping_type not in base_rates:
print(f"DEBUG: Invalid shipping type: {shipping_type}")
print(f"DEBUG: Available types: {list(base_rates.keys())}")
return None
rate = base_rates[shipping_type]
print(f"DEBUG: Base rate for {shipping_type}: ${rate}")
# Calculate cost
cost = weight * distance * rate
print(f"DEBUG: Calculation: {weight} * {distance} * {rate} = {cost}")
# Apply minimum charge
minimum_charge = 5.00
if cost < minimum_charge:
print(f"DEBUG: Cost {cost} below minimum {minimum_charge}, adjusting")
cost = minimum_charge
print(f"DEBUG: Final cost: ${cost}")
return cost
# Test with different parameter types
print("=== Testing shipping cost function ===")
# Test 1: Normal case
cost1 = calculate_shipping_cost(2.5, 100, "standard")
print(f"Result 1: ${cost1}\n")
# Test 2: Invalid weight
cost2 = calculate_shipping_cost(-1, 100, "standard")
print(f"Result 2: {cost2}\n")
# Test 3: Invalid shipping type
cost3 = calculate_shipping_cost(2.5, 100, "super_fast")
print(f"Result 3: {cost3}\n")
Debugging Return Values¶
def process_student_record(student_data):
"""Process student record - debug return values."""
print(f"DEBUG: Processing student: {student_data}")
# Extract data
name = student_data.get("name", "Unknown")
scores = student_data.get("scores", [])
print(f"DEBUG: Extracted name: '{name}'")
print(f"DEBUG: Extracted scores: {scores}")
# Validate scores
if not scores:
print("DEBUG: No scores provided, returning None")
return None
# Calculate average
total = sum(scores)
count = len(scores)
average = total / count
print(f"DEBUG: Total: {total}, Count: {count}, Average: {average}")
# Determine grade
if average >= 90:
letter_grade = "A"
elif average >= 80:
letter_grade = "B"
elif average >= 70:
letter_grade = "C"
elif average >= 60:
letter_grade = "D"
else:
letter_grade = "F"
print(f"DEBUG: Assigned letter grade: {letter_grade}")
# Create result
result = {
"name": name,
"average": round(average, 2),
"letter_grade": letter_grade,
"scores": scores
}
print(f"DEBUG: Returning result: {result}")
return result
def process_class_records(class_data):
"""Process multiple student records - debug function interactions."""
print(f"DEBUG: Processing {len(class_data)} students")
results = []
for i, student in enumerate(class_data):
print(f"\nDEBUG: Processing student {i + 1}:")
# Call helper function
processed = process_student_record(student)
if processed:
results.append(processed)
print(f"DEBUG: Added to results: {processed['name']}")
else:
print(f"DEBUG: Skipped invalid student record")
print(f"\nDEBUG: Final results count: {len(results)}")
return results
# Test data
test_class = [
{"name": "Alice", "scores": [92, 88, 95]},
{"name": "Bob", "scores": [87, 91, 84]},
{"name": "Charlie", "scores": []}, # Invalid - no scores
{"name": "Diana", "scores": [95, 93, 97]}
]
# Process with debugging
class_results = process_class_records(test_class)
print(f"\nFinal output: {len(class_results)} valid student records processed")
Advanced Debugging Techniques¶
Debugging with Assert Statements¶
def calculate_quadratic_roots(a, b, c):
"""Calculate roots of quadratic equation with assertions."""
# Use assertions to check preconditions
assert a != 0, f"Coefficient 'a' cannot be zero (got {a})"
assert isinstance(a, (int, float)), f"Coefficient 'a' must be a number (got {type(a)})"
assert isinstance(b, (int, float)), f"Coefficient 'b' must be a number (got {type(b)})"
assert isinstance(c, (int, float)), f"Coefficient 'c' must be a number (got {type(c)})"
print(f"DEBUG: Calculating roots for {a}x² + {b}x + {c} = 0")
# Calculate discriminant
discriminant = b * b - 4 * a * c
print(f"DEBUG: Discriminant = {b}² - 4({a})({c}) = {discriminant}")
# Check discriminant
if discriminant < 0:
print("DEBUG: Negative discriminant - no real roots")
return None, None
elif discriminant == 0:
print("DEBUG: Zero discriminant - one repeated root")
root = -b / (2 * a)
return root, root
else:
print("DEBUG: Positive discriminant - two distinct roots")
import math
sqrt_discriminant = math.sqrt(discriminant)
root1 = (-b + sqrt_discriminant) / (2 * a)
root2 = (-b - sqrt_discriminant) / (2 * a)
# Assert postconditions
assert root1 is not None, "Root1 should not be None"
assert root2 is not None, "Root2 should not be None"
return root1, root2
# Test with assertions
try:
roots = calculate_quadratic_roots(1, -5, 6) # x² - 5x + 6 = 0
print(f"Roots: {roots}")
except AssertionError as e:
print(f"Assertion failed: {e}")
Debugging with Try-Except Blocks¶
def robust_division_calculator():
"""Calculator that handles and debugs errors gracefully."""
while True:
print("\n=== Division Calculator ===")
print("Enter 'quit' to exit")
try:
# Get first number
num1_input = input("Enter first number: ")
if num1_input.lower() == 'quit':
break
num1 = float(num1_input)
print(f"DEBUG: First number parsed: {num1}")
# Get second number
num2_input = input("Enter second number: ")
if num2_input.lower() == 'quit':
break
num2 = float(num2_input)
print(f"DEBUG: Second number parsed: {num2}")
# Check for division by zero
if num2 == 0:
print("DEBUG: Division by zero detected")
print("Error: Cannot divide by zero!")
continue
# Perform division
result = num1 / num2
print(f"DEBUG: Calculation: {num1} / {num2} = {result}")
print(f"Result: {result}")
except ValueError as e:
print(f"DEBUG: ValueError caught: {e}")
print("Error: Please enter valid numbers!")
except KeyboardInterrupt:
print(f"\nDEBUG: KeyboardInterrupt caught")
print("Exiting calculator...")
break
except Exception as e:
print(f"DEBUG: Unexpected error: {type(e).__name__}: {e}")
print("An unexpected error occurred!")
# Run the robust calculator
# robust_division_calculator() # Uncomment to test
Debugging Best Practices¶
1. Systematic Debugging Approach¶
def debug_systematically():
"""Demonstrate systematic debugging approach."""
print("=== Systematic Debugging Steps ===")
print("1. Reproduce the bug consistently")
print("2. Isolate the problem area")
print("3. Form a hypothesis about the cause")
print("4. Test the hypothesis")
print("5. Fix the bug")
print("6. Test that the fix works")
print("7. Make sure you didn't break anything else")
def buggy_average_calculator(numbers):
"""Function with intentional bug for demonstration."""
# Step 1: Reproduce the bug
print(f"Input: {numbers}")
# Step 2: Isolate - the bug is in this function
total = 0
count = 0
for num in numbers:
total += num
count += 1
# Step 3: Hypothesis - maybe division by zero?
# Step 4: Test with debugging
print(f"DEBUG: total = {total}, count = {count}")
if count > 0:
average = total / count
print(f"DEBUG: average = {average}")
return average
else:
print("DEBUG: count is 0, returning 0")
return 0 # Step 5: This might be wrong - should it be None?
# Step 6: Test the fix
test_cases = [
[1, 2, 3, 4, 5], # Normal case
[], # Empty list
[10], # Single element
[-1, 0, 1] # Mix of negative, zero, positive
]
for test in test_cases:
result = buggy_average_calculator(test)
print(f"Average of {test}: {result}\n")
2. Debugging Checklist¶
def debugging_checklist():
"""Checklist for effective debugging."""
checklist = [
"✓ Can you reproduce the bug consistently?",
"✓ Do you understand what the code is supposed to do?",
"✓ Have you checked for common errors (typos, off-by-one, etc.)?",
"✓ Have you tested with different inputs?",
"✓ Are you using appropriate debugging tools?",
"✓ Have you traced through the code step by step?",
"✓ Are you making assumptions that might be wrong?",
"✓ Have you asked someone else to look at it?",
"✓ Did you test your fix thoroughly?",
"✓ Did you document what caused the bug?"
]
print("=== Debugging Checklist ===")
for item in checklist:
print(item)
debugging_checklist()
Summary¶
Debugging is a systematic process that requires patience and the right tools:
Key Debugging Techniques¶
-
Print Statements: Quick and universal debugging method
-
Breakpoints: Pause execution to inspect program state
-
Single Line Stepping: Execute code line by line
-
Watches: Monitor specific variables during execution
-
Function Interface Debugging: Trace parameter passing and return values
Essential Skills¶
-
Reproduce bugs consistently before trying to fix them
-
Isolate the problem to the smallest possible code section
-
Use scientific method: hypothesis, test, conclude
-
Test thoroughly after making fixes
-
Document your findings to prevent similar bugs
IDE Debugger Features¶
-
Breakpoints: Click in margin to pause execution
-
Step Over/Into/Out: Control execution flow
-
Variable inspection: See current values
-
Call stack: Understand function call hierarchy
-
Conditional breakpoints: Pause only when conditions are met
Remember: debugging is a skill that improves with practice. Every bug you fix makes you a better programmer!