🔍 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:
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.
Print Debugging
The oldest and most universal debugging technique: add print statements to see what your code is actually doing. It's simple, it works everywhere, and even senior developers use it daily.
# Bug: this function returns wrong results for some inputs
def find_longest_word(sentence):
words = sentence.split()
longest = ""
for word in words:
if len(word) > len(longest):
longest = word
return longest
# It works for "hello world" but fails for "hello, world!"
# Let's debug with prints:
def find_longest_word(sentence):
words = sentence.split()
print(f"DEBUG words: {words}") # See what split() produces
longest = ""
for word in words:
print(f"DEBUG checking '{word}' (len={len(word)}) vs '{longest}' (len={len(longest)})")
if len(word) > len(longest):
longest = word
print(f"DEBUG → new longest: '{longest}'")
print(f"DEBUG result: '{longest}'")
return longest
# Test it:
result = find_longest_word("I love programming!")
# DEBUG words: ['I', 'love', 'programming!']
# DEBUG checking 'I' (len=1) vs '' (len=0)
# DEBUG → new longest: 'I'
# DEBUG checking 'love' (len=4) vs 'I' (len=1)
# DEBUG → new longest: 'love'
# DEBUG checking 'programming!' (len=12) vs 'love' (len=4)
# DEBUG → new longest: 'programming!'
# DEBUG result: 'programming!'
#
# AHA! 'programming!' includes the punctuation!
# Fix: strip punctuation from words before comparing
// Bug: discount calculation sometimes returns negative prices
function applyDiscount(price, discountPercent) {
let discount = price * discountPercent;
let finalPrice = price - discount;
return finalPrice;
}
// applyDiscount(100, 20) should give 80 but gives -1900!
// Let's debug with console.log:
function applyDiscount(price, discountPercent) {
console.log("DEBUG inputs:", { price, discountPercent });
let discount = price * discountPercent;
console.log("DEBUG discount amount:", discount);
let finalPrice = price - discount;
console.log("DEBUG final price:", finalPrice);
return finalPrice;
}
applyDiscount(100, 20);
// DEBUG inputs: { price: 100, discountPercent: 20 }
// DEBUG discount amount: 2000 ← There it is!
// DEBUG final price: -1900
//
// AHA! discountPercent is 20, not 0.20
// Fix: divide by 100, or expect 0.20 as input
// Pro tip: console methods for better output
console.log("simple message");
console.warn("⚠️ warning"); // Yellow in browser
console.error("❌ error"); // Red in browser
console.table([{a: 1}, {a: 2}]); // Formatted table
console.group("Section"); // Collapsible group
console.log("detail 1");
console.log("detail 2");
console.groupEnd();
// Bug: search always returns "not found" even for existing items
static int FindIndex(string[] items, string target)
{
for (int i = 0; i < items.Length; i++)
{
if (items[i] == target)
return i;
}
return -1;
}
string[] names = { "Alice", "Bob", "Charlie" };
int index = FindIndex(names, "bob"); // Returns -1, but Bob is there!
// Let's debug with Console.WriteLine:
static int FindIndex(string[] items, string target)
{
Console.WriteLine($"DEBUG searching for: '{target}'");
for (int i = 0; i < items.Length; i++)
{
Console.WriteLine($"DEBUG [{i}] comparing '{items[i]}' == '{target}' → {items[i] == target}");
if (items[i] == target)
return i;
}
Console.WriteLine("DEBUG not found!");
return -1;
}
FindIndex(names, "bob");
// DEBUG searching for: 'bob'
// DEBUG [0] comparing 'Alice' == 'bob' → False
// DEBUG [1] comparing 'Bob' == 'bob' → False ← AHA!
// DEBUG [2] comparing 'Charlie' == 'bob' → False
// DEBUG not found!
//
// 'Bob' != 'bob' — case-sensitive comparison!
// Fix: use StringComparison.OrdinalIgnoreCase
// C# also has Debug.WriteLine (only runs in Debug mode):
// System.Diagnostics.Debug.WriteLine("Debug only message");
💡 Print Debugging Best Practices
- Prefix with DEBUG — makes them easy to find and remove later
- Print variable names AND values —
f"DEBUG x={x}"not justprint(x) - Print at decision points — before/after conditionals and loops
- Print at function entry/exit — see the inputs and the return value
- Remove them when done — or use a logging library for permanent debug output
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.
Binary Search Debugging
When you have a big program and something is wrong but you don't know where, use the binary search method: check the middle of the code, determine which half contains the bug, then repeat.
Bug is somewhere..."] --> B["Check line 50:
Is data correct here?"] B -->|"Yes — data is good"| C["Bug is in lines 51-100"] B -->|"No — data is wrong"| D["Bug is in lines 1-50"] C --> E["Check line 75"] D --> F["Check line 25"] E --> G["Narrowing down..."] F --> G G --> H["Found it! Line 37"] style A fill:#ef4444,color:#fff style H fill:#22c55e,color:#fff
# This pipeline produces the wrong final result.
# Where does it go wrong?
def load_data():
return [
{"name": "Widget", "price": "9.99", "qty": "5"},
{"name": "Gadget", "price": "24.99", "qty": "2"},
{"name": "Gizmo", "price": "14.99", "qty": "3"},
]
def clean_data(raw):
cleaned = []
for item in raw:
cleaned.append({
"name": item["name"],
"price": float(item["price"]),
"qty": int(item["qty"]),
})
return cleaned
def calculate_totals(items):
for item in items:
item["total"] = item["price"] + item["qty"] # Bug here!
return items
def apply_tax(items, rate=0.08):
for item in items:
item["total"] = item["total"] * (1 + rate)
return items
def generate_report(items):
grand_total = sum(item["total"] for item in items)
return f"Grand total: ${grand_total:.2f}"
# Pipeline
raw = load_data()
cleaned = clean_data(raw)
# --- STEP 1: Check the midpoint ---
print(f"CHECKPOINT after clean_data: {cleaned}")
# Looks good! Prices and quantities are correct numbers.
totals = calculate_totals(cleaned)
# --- STEP 2: Bug is in second half — check after calculate_totals ---
print(f"CHECKPOINT after calculate_totals: {totals}")
# [{'name': 'Widget', 'total': 14.99}, ...]
# WAIT — 9.99 + 5 = 14.99? That's price + qty, not price * qty!
# Found it! Line: item["total"] = item["price"] + item["qty"]
# Should be: item["total"] = item["price"] * item["qty"]
// Same pipeline concept in JS — binary search for the bug
function loadData() {
return [
{ name: "Widget", price: "9.99", qty: "5" },
{ name: "Gadget", price: "24.99", qty: "2" },
{ name: "Gizmo", price: "14.99", qty: "3" },
];
}
function cleanData(raw) {
return raw.map(item => ({
name: item.name,
price: parseFloat(item.price),
qty: parseInt(item.qty),
}));
}
function calculateTotals(items) {
return items.map(item => ({
...item,
total: item.price + item.qty, // Bug: should be *
}));
}
function applyTax(items, rate = 0.08) {
return items.map(item => ({
...item,
total: item.total * (1 + rate),
}));
}
// Binary search: check the midpoint first
let raw = loadData();
let cleaned = cleanData(raw);
console.log("CHECKPOINT cleaned:", cleaned); // Looks good ✅
let totals = calculateTotals(cleaned);
console.log("CHECKPOINT totals:", totals); // Bug found! ❌
// total: 14.99 for Widget → price + qty, not price * qty
// Binary search debugging in C#
// Check data at the midpoint of your pipeline
var raw = LoadData();
var cleaned = CleanData(raw);
// CHECKPOINT 1: Is data correct here?
Console.WriteLine("CHECKPOINT cleaned:");
foreach (var item in cleaned)
Console.WriteLine($" {item.Name}: ${item.Price} x {item.Qty}");
// Looks correct ✅
var totals = CalculateTotals(cleaned);
// CHECKPOINT 2: Check after calculate
Console.WriteLine("CHECKPOINT totals:");
foreach (var item in totals)
Console.WriteLine($" {item.Name}: total=${item.Total}");
// Widget: total=$14.99 — that's price + qty, not price * qty! ❌
// Bug found in CalculateTotals!
// item.Total = item.Price + item.Qty; // Wrong!
// item.Total = item.Price * item.Qty; // Correct!
💡 Why Binary Search Works
If your program has 8 stages and the output of stage 8 is wrong, checking all 8 stages takes up to 8 checks. Binary search finds it in 3 checks: check stage 4, then stage 2 or 6, then the exact stage. For larger programs, the savings are dramatic — 1000 lines of code takes at most ~10 checkpoints instead of reading every line.
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
- Place a rubber duck on your desk (or use a coffee mug, a pet, anything)
- Explain the purpose of the code to the duck — "This function should calculate the total price of items in a cart."
- Go line by line — "First, I initialize
totalto zero. Then I loop over each item. For each item, I multiply price times quantity and add it to total. Then I return total." - At some point you'll say: "Wait... I'm adding
price + quantitybut I should be addingprice * quantity..." - 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:
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?