Skip to content

This textbook is in beta – content is actively being refined. Report issues or suggestions

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

  1. Print Statements: Quick and universal debugging method

  2. Breakpoints: Pause execution to inspect program state

  3. Single Line Stepping: Execute code line by line

  4. Watches: Monitor specific variables during execution

  5. 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!