Skip to main content

🔍 Lesson 7.3: Debugging Strategies

Your code doesn't work. There's no error message — it just does the wrong thing. Or maybe there is an error message, but it points to a line that looks perfectly fine. Welcome to debugging — the skill you'll use more than any other in your programming career. This lesson isn't about memorizing a tool; it's about developing a systematic mindset for finding and fixing problems.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Read error messages effectively — extract the useful parts
  • Use print/log debugging to trace program flow and variable state
  • Use debugger tools (breakpoints, stepping, variable inspection)
  • Apply the binary search method to narrow down bugs
  • Use rubber duck debugging to think through problems
  • Follow a systematic debugging process instead of guessing
  • Identify the most common bug categories and where to look first

Estimated Time: 45 minutes

Project: Debug a series of intentionally broken programs

📑 In This Lesson

Reading Error Messages

Error messages look intimidating, but they follow a predictable structure. Every error message tells you three things:

flowchart LR A["WHERE
File + Line"] --> B["WHAT
Error Type"] --> C["WHY
Error Message"] style A fill:#3b82f6,color:#fff style B fill:#ef4444,color:#fff style C fill:#22c55e,color:#fff
# Running this broken code:
def calculate_average(numbers):
    total = sum(numbers)
    return total / len(numbers)

scores = []
avg = calculate_average(scores)

# Python's error message (read BOTTOM-UP!):
#
# Traceback (most recent call last):          ← Start of trace
#   File "grades.py", line 6, in <module>     ← WHERE (caller)
#     avg = calculate_average(scores)
#   File "grades.py", line 3, in calculate_average  ← WHERE (exact)
#     return total / len(numbers)              ← The line that failed
# ZeroDivisionError: division by zero          ← WHAT + WHY
#
# Reading strategy:
# 1. Start at the BOTTOM: ZeroDivisionError: division by zero
# 2. Look UP for YOUR code (skip library files)
# 3. Line 3: return total / len(numbers) — dividing by len([]) which is 0
# 4. Line 6 shows HOW we got there: empty list passed in
// Running this broken code:
function calculateAverage(numbers) {
    let total = numbers.reduce((sum, n) => sum + n);
    return total / numbers.length;
}

let scores = null;
let avg = calculateAverage(scores);

// JavaScript's error message:
//
// TypeError: Cannot read properties of null (reading 'reduce')
//     at calculateAverage (grades.js:2:28)    ← WHERE (exact)
//     at Object.<anonymous> (grades.js:7:11)  ← WHERE (caller)
//
// Reading strategy:
// 1. First line: TypeError — trying to use null as an object
// 2. "reading 'reduce'" — the .reduce() call failed
// 3. grades.js:2:28 — line 2, column 28
// 4. Ask: why is 'numbers' null? → line 7 passes null

// Node.js shows the code line too:
// grades.js:2
//     let total = numbers.reduce((sum, n) => sum + n);
//                         ^
// TypeError: Cannot read properties of null (reading 'reduce')
// Running this broken code:
static double CalculateAverage(int[] numbers)
{
    int total = numbers.Sum();
    return total / numbers.Length;
}

int[] scores = null;
double avg = CalculateAverage(scores);

// C#'s error message:
//
// Unhandled exception. System.NullReferenceException:
//   Object reference not set to an instance of an object.
//    at Program.CalculateAverage(Int32[] numbers)
//       in /app/Program.cs:line 4              ← WHERE (exact)
//    at Program.Main(String[] args)
//       in /app/Program.cs:line 9              ← WHERE (caller)
//
// Reading strategy:
// 1. Exception type: NullReferenceException
// 2. Line 4: numbers.Sum() — numbers is null
// 3. Line 9: passed null to the function
// 4. Fix: add a null check or initialize the array

✅ Error Message Reading Checklist

  • Read from the bottom up (Python) or top down (JS/C#) — find the error type and message first
  • Find YOUR code in the stack trace — skip library/framework lines
  • Look at the actual line — what could go wrong there?
  • Trace backwards — how did the bad value get to that line?
  • Google the error type + message if you're stuck — someone has had this exact problem before
🎓 Instructor Note: Delivery Guidance

Error messages are where beginners freeze up most. Show a real error on screen and physically walk through it: "First I look at the bottom — ZeroDivisionError. Now I know what happened. Then I look at the line — return total / len(numbers). What could be zero here? len(numbers)! Now I trace back — what was passed in? An empty list." Model this thinking process out loud. It's not intuitive — students need to see an expert read errors to learn the strategy. Python's "read bottom-up" vs. JS/C#'s "read top-down" is a key cross-language difference to highlight.

Debugger Tools

Print debugging works, but for complex problems you need more power. Every language has a debugger — a tool that lets you pause your program, inspect every variable, and step through code one line at a time.

# Method 1: Insert a breakpoint in your code
def process_order(items, tax_rate):
    subtotal = sum(item["price"] * item["qty"] for item in items)

    breakpoint()  # Program PAUSES here! (Python 3.7+)
    # Older Python: import pdb; pdb.set_trace()

    tax = subtotal * tax_rate
    total = subtotal + tax
    return total

# When the program pauses, you get a (Pdb) prompt:
#
# (Pdb) p subtotal          ← Print a variable
# 59.97
# (Pdb) p items             ← Print the list
# [{'name': 'Widget', 'price': 9.99, 'qty': 3}, ...]
# (Pdb) p tax_rate           ← Check the tax rate
# 0.08
# (Pdb) n                    ← Next line (step over)
# (Pdb) s                    ← Step into function call
# (Pdb) c                    ← Continue (run until next breakpoint)
# (Pdb) l                    ← List source code around current line
# (Pdb) q                    ← Quit debugger

# Method 2: Run with debugger from the start
# python -m pdb your_script.py

# Method 3: VS Code (recommended for beginners)
# 1. Click the line number gutter to set a breakpoint (red dot)
# 2. Press F5 (Run → Start Debugging)
# 3. Use the debug toolbar: Step Over (F10), Step Into (F11),
#    Continue (F5), Stop (Shift+F5)
# 4. Hover over variables to see their values
# 5. Use the Variables panel and Watch panel
// Method 1: Insert a breakpoint in your code
function processOrder(items, taxRate) {
    let subtotal = items.reduce((sum, item) =>
        sum + item.price * item.qty, 0);

    debugger;  // Browser DevTools PAUSE here!

    let tax = subtotal * taxRate;
    let total = subtotal + tax;
    return total;
}

// When paused in Chrome/Firefox DevTools:
//
// Sources tab → your code is highlighted at the debugger line
// Scope panel → shows all local variables and their values
// Console → you can type any expression to evaluate it
//
// Controls:
//   F10 / Step Over    → Execute current line, move to next
//   F11 / Step Into    → Jump into function call
//   Shift+F11 / Out   → Run until current function returns
//   F8 / Continue      → Run until next breakpoint
//
// Watch panel → add expressions to monitor (e.g., items.length)
// Call Stack → see how you got to this point

// Method 2: Set breakpoints in DevTools
// 1. Open DevTools (F12 or Ctrl+Shift+I)
// 2. Go to Sources tab
// 3. Click a line number to set a breakpoint
// 4. Reload the page — execution pauses at the breakpoint

// Method 3: Conditional breakpoints
// Right-click a line number → "Add conditional breakpoint"
// Example: total > 1000  (only pauses when total exceeds 1000)
// Method 1: Debugger.Break() in code
static decimal ProcessOrder(List<OrderItem> items, decimal taxRate)
{
    decimal subtotal = items.Sum(i => i.Price * i.Qty);

    System.Diagnostics.Debugger.Break();  // Pauses if debugger attached

    decimal tax = subtotal * taxRate;
    decimal total = subtotal + tax;
    return total;
}

// Method 2: VS Code / Visual Studio (recommended)
//
// 1. Click the line number gutter → red dot (breakpoint)
// 2. Press F5 → Start Debugging
// 3. When paused:
//    - Hover over any variable to see its value
//    - Variables panel shows all locals
//    - Watch panel for custom expressions
//    - Immediate Window to evaluate code on the fly
//
// Stepping:
//    F10 → Step Over (execute line, don't enter functions)
//    F11 → Step Into (enter function calls)
//    Shift+F11 → Step Out (run to end of current function)
//    F5 → Continue (run to next breakpoint)
//
// Pro features:
//    - Conditional breakpoints: right-click → Condition → total > 1000
//    - Hit count breakpoints: break after N hits
//    - Logpoints: print without pausing (like smart print debugging)

// Method 3: Logging with conditional compilation
#if DEBUG
    Console.WriteLine($"DEBUG subtotal: {subtotal}");
#endif
// This code ONLY runs in Debug builds, not Release builds

Debugger Commands Quick Reference

Action 🐍 Python (pdb) ⚡ JS (DevTools) 🔷 C# (VS Code)
Set breakpoint in code breakpoint() debugger; Debugger.Break()
Step over n (next) F10 F10
Step into s (step) F11 F11
Continue c (continue) F8 F5
Print variable p variable Hover / Console Hover / Immediate
Show code context l (list) Sources panel Editor highlights
🎓 Instructor Note: Delivery Guidance

If possible, do a live demo of the VS Code debugger — it's the most visual and beginner-friendly tool across all three languages. Set a breakpoint, run in debug mode, and hover over variables. Students are often amazed that they can see every variable's value in real time. For JavaScript, open Chrome DevTools and demonstrate the Sources tab with debugger;. For Python, show breakpoint() in the terminal — it's less visual but works everywhere. Emphasize that the debugger is not "advanced" — it's a fundamental tool that should be learned early. The sooner students are comfortable with it, the faster they'll debug everything.

Rubber Duck Debugging

This technique sounds silly but is remarkably effective: explain your code, line by line, to a rubber duck (or any inanimate object, pet, or patient friend). The act of explaining forces you to slow down and articulate what each line should do — and you often spot the mismatch between what it should do and what it actually does.

🦆 The Rubber Duck Method

  1. Place a rubber duck on your desk (or use a coffee mug, a pet, anything)
  2. Explain the purpose of the code to the duck — "This function should calculate the total price of items in a cart."
  3. Go line by line — "First, I initialize total to zero. Then I loop over each item. For each item, I multiply price times quantity and add it to total. Then I return total."
  4. At some point you'll say: "Wait... I'm adding price + quantity but I should be adding price * quantity..."
  5. Thank the duck.

This works because reading code is passive — your eyes skim over problems. Explaining code is active — you have to think about what each line does and whether that matches your intent. The bug often lives in the gap between what you think the code does and what it actually does.

💡 Modern Variations

  • Write a comment for every line — same effect as explaining to a duck, but in text
  • Explain it to a coworker — "pair debugging" is even more effective because they ask questions
  • Write a forum post describing the problem — many developers solve their issue while writing the question (this is so common that Stack Overflow has a name for it: "rubber duck debugging")
  • Step away for 15 minutes — fresh eyes catch things tired eyes miss
🎓 Instructor Note: Delivery Guidance

If you can, bring an actual rubber duck to class — it always gets a laugh and makes the concept memorable. Have a student explain a buggy code snippet line by line to the duck. The class will spot the bug together as it's being explained. This technique is especially valuable because it requires no tools — it works at 2 AM when you're frustrated and nothing else is working. It also transfers to all languages and all types of bugs. The "write a forum post" variation is worth emphasizing — many students are surprised to learn that the act of formulating a good question often solves the problem.

The Systematic Process

Resist the urge to randomly change code and hope for the best. Follow a process:

flowchart TD A["1. REPRODUCE
Make the bug happen reliably"] --> B["2. ISOLATE
Find the smallest code that triggers it"] B --> C["3. IDENTIFY
Pinpoint the exact line/condition"] C --> D["4. FIX
Change the minimum amount of code"] D --> E["5. VERIFY
Confirm the fix AND check for side effects"] E --> F["6. REFLECT
Why did this bug happen? Prevent similar ones"] style A fill:#3b82f6,color:#fff style D fill:#f59e0b,color:#fff style E fill:#22c55e,color:#fff
Step What To Do Common Mistake
1. Reproduce Find exact inputs that cause the bug every time Trying to fix a bug you can't reliably trigger
2. Isolate Simplify — remove unrelated code until you have the minimal case Searching in a file with 500 lines instead of extracting the 10 that matter
3. Identify Use prints, debugger, or binary search to find the exact line Guessing instead of systematically narrowing down
4. Fix Change as little as possible — one fix at a time Changing 5 things at once so you don't know which one worked
5. Verify Test the original failing case AND nearby cases Testing only the one case that was broken
6. Reflect Ask "How could I prevent this type of bug?" Moving on without learning from the experience

⚠️ The #1 Debugging Anti-Pattern: "Shotgun Debugging"

Randomly changing code to see if the problem goes away is shotgun debugging. It's tempting because it feels like action, but it's ineffective and dangerous — you might "fix" the symptom while introducing new bugs. Always understand the problem before changing code. If you can't explain why your fix works, you haven't actually fixed it.

Common Bug Categories

Most bugs fall into a small number of categories. Knowing the usual suspects lets you check them first.

Bug Category Example Where to Look
Off-by-one Loop runs 9 times instead of 10; array index out of range Loop bounds (< vs <=), array indices, string slicing
Wrong operator + instead of *; = instead of == Math expressions, comparisons, assignments
Type mismatch "5" + 3 gives "53" instead of 8 User input (always strings!), JSON data, API responses
Null/undefined Accessing property on null or undefined Function returns, optional values, uninitialized variables
Scope issues Variable exists but holds the wrong value; shadowed variable Functions, loops, nested blocks, closures
State mutation Changing a shared list/object from one place affects another Mutable defaults, shared references, aliased objects
Case sensitivity "Bob" != "bob" in a search String comparisons, dictionary keys, file paths
Async timing Using data before it's loaded; race conditions API calls, file reads, event handlers, callbacks
# 1. Off-by-one
for i in range(10):    # 0-9, not 1-10!
    print(i)

# 2. Mutable default argument (Python-specific trap!)
def add_item(item, items=[]):    # ❌ Shared across ALL calls!
    items.append(item)
    return items

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['a', 'b'] — Wait, where did 'a' come from?!

# Fix:
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

# 3. String/number confusion
age = input("Your age: ")  # ALWAYS returns a string!
if age > 18:               # ❌ Comparing string to int!
    print("Adult")
# Fix: age = int(input("Your age: "))
// 1. Type coercion surprises
console.log("5" + 3);     // "53" (string concatenation!)
console.log("5" - 3);     // 2   (numeric subtraction)
console.log("5" == 5);    // true  (loose equality — coerces type)
console.log("5" === 5);   // false (strict equality — safe!)

// 2. this context
class Timer {
    constructor() { this.seconds = 0; }
    start() {
        setInterval(function() {
            this.seconds++;  // ❌ 'this' is NOT the Timer!
            console.log(this.seconds);  // NaN
        }, 1000);
    }
}
// Fix: use arrow function (inherits 'this')
// setInterval(() => { this.seconds++; }, 1000);

// 3. Equality gotchas
console.log([] == false);   // true  (wat?)
console.log(null == undefined);  // true
console.log(NaN == NaN);   // false (NaN is not equal to anything!)
// Fix: ALWAYS use === and !== in JavaScript
// 1. Integer division truncation
int a = 7, b = 2;
Console.WriteLine(a / b);       // 3 (not 3.5!)
Console.WriteLine(a / (double)b);  // 3.5 — cast to double first

// 2. String comparison (case sensitivity)
string name = "Alice";
if (name == "alice")  // False! Case-sensitive by default
    Console.WriteLine("Found");
// Fix:
if (name.Equals("alice", StringComparison.OrdinalIgnoreCase))
    Console.WriteLine("Found");

// 3. Reference equality vs value equality
int[] a1 = { 1, 2, 3 };
int[] a2 = { 1, 2, 3 };
Console.WriteLine(a1 == a2);             // False! Different objects
Console.WriteLine(a1.SequenceEqual(a2)); // True — compares contents

// 4. Null reference (the billion-dollar mistake)
string? text = null;
Console.WriteLine(text.Length);  // NullReferenceException!
// Fix:
Console.WriteLine(text?.Length ?? 0);  // 0 (null-safe)
🎓 Instructor Note: Delivery Guidance

The "Usual Suspects" code examples are designed to be run live — let students see each bug in action. Python's mutable default argument is one of the most infamous Python gotchas and always surprises students. JavaScript's type coercion ("5" + 3 = "53") is a classic. C#'s integer division truncation bites even experienced developers. Walk through each example and ask students to predict the output before running it. The table of common bug categories is worth printing out or posting — when stuck, students can scan it as a checklist: "Is this an off-by-one? A type mismatch? A scope issue?" Having a vocabulary for bug types speeds up debugging enormously.

Exercises

🏋️ Exercise 1: Bug Hunt

Objective: Each code snippet below has exactly one bug. Find it, explain what's wrong, and fix it. Use any debugging technique from this lesson.

🐛 Bug 1: Average Calculator
# This should return the average, but returns wrong values
def average(numbers):
    total = 0
    for num in numbers:
        total += num
        avg = total / len(numbers)
    return avg

print(average([10, 20, 30]))  # Should be 20.0, gets 10.0
function average(numbers) {
    let total = 0;
    for (let num of numbers) {
        total += num;
        let avg = total / numbers.length;
    }
    return avg;  // Might have a scope issue too!
}
console.log(average([10, 20, 30]));  // Should be 20
static double Average(int[] numbers)
{
    int total = 0;
    double avg = 0;
    foreach (int num in numbers)
    {
        total += num;
        avg = total / numbers.Length;  // Computed inside loop!
    }
    return avg;
}
Console.WriteLine(Average(new[] { 10, 20, 30 })); // Should be 20
✅ Answer

Bug: The average calculation avg = total / len(numbers) is inside the loop. It gets recalculated on every iteration, but the total is incomplete until the loop finishes. In the Python version, the indentation puts the calculation inside the for loop. Move it after the loop. (In JS, there's also a scope issue — let avg inside the loop isn't accessible outside.)

Fix: Move avg = total / len(numbers) outside the loop, after it completes. Also in C#, use (double)total / numbers.Length to avoid integer division.

🐛 Bug 2: FizzBuzz
# FizzBuzz: print "Fizz" for multiples of 3, "Buzz" for 5, "FizzBuzz" for both
def fizzbuzz(n):
    results = []
    for i in range(1, n + 1):
        if i % 3 == 0:
            results.append("Fizz")
        elif i % 5 == 0:
            results.append("Buzz")
        elif i % 15 == 0:
            results.append("FizzBuzz")
        else:
            results.append(str(i))
    return results

# fizzbuzz(15) should end with "FizzBuzz" but ends with "Fizz"
function fizzbuzz(n) {
    let results = [];
    for (let i = 1; i <= n; i++) {
        if (i % 3 === 0) {
            results.push("Fizz");
        } else if (i % 5 === 0) {
            results.push("Buzz");
        } else if (i % 15 === 0) {
            results.push("FizzBuzz");
        } else {
            results.push(String(i));
        }
    }
    return results;
}
static List<string> FizzBuzz(int n)
{
    var results = new List<string>();
    for (int i = 1; i <= n; i++)
    {
        if (i % 3 == 0)
            results.Add("Fizz");
        else if (i % 5 == 0)
            results.Add("Buzz");
        else if (i % 15 == 0)
            results.Add("FizzBuzz");
        else
            results.Add(i.ToString());
    }
    return results;
}
✅ Answer

Bug: The i % 15 check comes last, but 15 is divisible by both 3 and 5 — so the i % 3 check catches it first and returns "Fizz" instead of "FizzBuzz". The elif/else if chain means the 15 check never runs for multiples of 15.

Fix: Check i % 15 (or i % 3 == 0 and i % 5 == 0) first, before the individual checks. Order matters in conditional chains!

🐛 Bug 3: Unique Values
# Should return only unique values from a list
def unique(items):
    result = []
    for item in items:
        if item not in items:
            result.append(item)
    return result

print(unique([1, 2, 2, 3, 3, 3]))  # Should be [1, 2, 3], gets []
function unique(items) {
    let result = [];
    for (let item of items) {
        if (!items.includes(item)) {
            result.push(item);
        }
    }
    return result;
}
console.log(unique([1, 2, 2, 3, 3, 3])); // Should be [1,2,3], gets []
static List<int> Unique(int[] items)
{
    var result = new List<int>();
    foreach (int item in items)
    {
        if (!items.Contains(item))
            result.Add(item);
    }
    return result;
}
// Unique([1, 2, 2, 3, 3, 3]) → should be [1,2,3], gets []
✅ Answer

Bug: The check is if item not in items — but we're checking if the item is in the original list (items), which it always is (because we're iterating over it!). Every item will be found in items, so nothing gets added to result.

Fix: Check if item not in result — whether the item is already in the output list, not the input list.

🏋️ Exercise 2: Debug the Pipeline

Objective: The function below is supposed to process a list of student grades and return a summary. It has 3 bugs. Use print debugging, the binary search method, or any technique to find and fix all three.

🐛 Buggy Code
def process_grades(students):
    """Process student grades and return a summary."""
    results = []

    for student in students:
        name = student["name"]
        grades = student["grades"]

        # Bug 1 is in this section
        total = 0
        for grade in grades:
            total += grade
        average = total / len(students)  # Hmm...

        # Bug 2 is in this section
        if average >= 90:
            letter = "A"
        elif average >= 80:
            letter = "B"
        elif average >= 70:
            letter = "C"
        elif average >= 60:
            letter = "D"
        else:
            letter = "F"

        # Bug 3 is in this section
        results.append({
            "name": name,
            "average": average,
            "letter": letter,
            "passing": letter != "F" or letter != "D"
        })

    return results

# Test data
students = [
    {"name": "Alice", "grades": [95, 87, 92, 98]},
    {"name": "Bob", "grades": [72, 65, 78, 70]},
    {"name": "Charlie", "grades": [55, 42, 61, 50]},
]

for result in process_grades(students):
    print(f"{result['name']}: avg={result['average']:.1f}, "
          f"grade={result['letter']}, passing={result['passing']}")
function processGrades(students) {
    let results = [];

    for (let student of students) {
        let { name, grades } = student;

        // Bug 1 is in this section
        let total = 0;
        for (let grade of grades) {
            total += grade;
        }
        let average = total / students.length;

        // Bug 2 is in this section
        let letter;
        if (average >= 90) letter = "A";
        else if (average >= 80) letter = "B";
        else if (average >= 70) letter = "C";
        else if (average >= 60) letter = "D";
        else letter = "F";

        // Bug 3 is in this section
        results.push({
            name,
            average,
            letter,
            passing: letter !== "F" || letter !== "D"
        });
    }
    return results;
}

let students = [
    { name: "Alice", grades: [95, 87, 92, 98] },
    { name: "Bob", grades: [72, 65, 78, 70] },
    { name: "Charlie", grades: [55, 42, 61, 50] },
];

processGrades(students).forEach(r =>
    console.log(`${r.name}: avg=${r.average.toFixed(1)}, ` +
                `grade=${r.letter}, passing=${r.passing}`));
// Same bugs — adapted to C# syntax
// Bug 1: Wrong denominator in average
// Bug 2: Letter grade logic looks correct... or does it?
// Bug 3: Boolean logic error in "passing" check

// Try adding Console.WriteLine checkpoints to find each bug!
// Fix all three, then verify with the test data.
✅ Solutions

Bug 1: average = total / len(students) divides by the number of students (3) instead of the number of grades for this student. Fix: average = total / len(grades)

Bug 2: The letter grade logic is actually correct! This is a red herring — not every section has a bug. (This teaches students to verify their assumptions rather than assume every section is broken.)

Bug 3: letter != "F" or letter != "D" is always True because of Boolean logic. If letter is "F", then letter != "D" is True (and vice versa). Fix: use and instead of or: letter != "F" and letter != "D"

🎓 Instructor Note: Delivery Guidance

Exercise 1 (Bug Hunt) is designed to train pattern recognition — each bug represents a common category from the table. Let students try to find each bug before revealing the answer. Exercise 2 is more realistic: a function with multiple bugs that interact with each other. Bug 2 being a "red herring" (no actual bug) is intentional — it teaches students not to assume every section is broken, and to verify their analysis before making changes. The or vs and bug in Bug 3 is one of the most common logical errors in programming and is worth spending extra time on. Ask students: "What does x != 'a' or x != 'b' evaluate to when x is 'a'? When x is 'b'? When x is 'c'?" Walk through the truth table to make it click.

Summary

🎉 Key Takeaways

  • Read error messages systematically: find the error type, then the line in YOUR code, then trace backwards
  • Print debugging is simple and universal — prefix with DEBUG, print names AND values, print at decision points
  • Debugger tools (breakpoints, stepping, variable inspection) are powerful for complex problems — learn VS Code's debugger early
  • Binary search debugging narrows down bugs fast — check the midpoint, determine which half is broken, repeat
  • Rubber duck debugging works because explaining activates different thinking than reading — no tools required
  • Follow a process: Reproduce → Isolate → Identify → Fix → Verify → Reflect
  • Know the common bugs: off-by-one, wrong operator, type mismatch, null/undefined, scope, case sensitivity
  • Never shotgun debug — understand the problem before changing code

🚀 What's Next?

That wraps up Module 7: Error Handling! You can now catch errors, create custom exceptions, and systematically debug your code. In Module 8, we move to Working with Data — reading and writing files, the first step toward programs that interact with the outside world.

🎯 Quick Check

Question 1: In a Python traceback, where should you look first?

Question 2: What is "rubber duck debugging"?

Question 3: What is the FIRST step in the systematic debugging process?