Debugging B: The Debugging Process and the VS Code Debugger

Lesson goals
  • Apply the five steps of debugging — Reproduce, Isolate, Root Cause, Fix, Verify
  • Set a Breakpoint and run code in Debug mode in VS Code
  • Use Step Over, Step Into and Continue, and follow variables in the Variables window
  • Find and fix a Logical Error in real code with the help of the Debugger

What will we learn?

In the previous lesson we learned to read errors. Now we will learn to hunt bugs, including ones that have no error message:

  1. A process — five fixed steps for every bug.
  2. A tool — the VS Code Debugger: stopping the code in the middle and checking what happens inside it.
  3. Real practice — together we will solve a bug step by step.

Why do we need this?

The beginners’ way is “guess and change”: change something, run, and if it did not work, change something else. It is slow, frustrating, and often creates a new bug. Professionals work with a method, and the Debugger lets you see the code “in slow motion”, instead of guessing what happens in it.

Explanation from the basics

The five steps of debugging

StepNameThe question it answers
1ReproduceHow do you make the bug happen every time? With which input?
2IsolateWhere in the code is the problem? Narrow it down to a line or a function.
3Root CauseWhy does it happen? Not the symptom, but the cause.
4FixFix the cause, and do not hide the symptom.
5VerifyIs the bug gone? And did nothing else break?
Analogy: a doctor

A good doctor does not give every patient a painkiller. They reproduce (“when does it hurt?”), isolate (“where exactly?”), find the cause (tests), treat the cause, and check at a follow-up visit that the patient recovered. A painkiller alone is “fixing the symptom”. The pain comes back, and the problem continues.

What is a Debugger?

A Debugger is a tool that runs the program under your control: it stops it at a line you chose, and lets you advance line by line and see the value of every variable at every moment.

The terms:

TermWhat it is
BreakpointA “stopping point”: a mark on a line. When the run reaches it, the program stops before executing the line.
Continue (F5)Continue running normally, until the next Breakpoint (or until the end).
Step Over (F10)Execute the current line in full and stop at the next line. If it contains a function call, the function runs “all at once”.
Step Into (F11)If the line contains a function call, go inside it and stop at its first line.
Step Out (Shift+F11)Finish the current function and return to whoever called it.
VariablesA window that shows all the variables and their values at this moment.
WatchExpressions you chose to follow, for example total / count.
Call StackThe chain of calls: which function called which. It is the Traceback, only “live”.
Stop (Shift+F5)Stop debugging.
Step Over versus Step Into

You are reading a recipe, and it has a line “prepare the dough (see page 40)”. Step Over = you prepare the dough without opening page 40, and move on to the next line of the recipe. Step Into = you open page 40 and read the dough recipe line by line.

Syntax

How to start the Debugger in VS Code:

  1. Make sure Microsoft’s Python extension is installed. It includes the Debugger.
  2. Open a .py file.
  3. Setting a Breakpoint: click in the left margin, to the left of the line number. A red dot 🔴 appears (or F9 on the line).
  4. Starting: F5, or the small arrow next to the ▷ button → Python Debugger: Debug Python File. If asked which configuration, choose Python File.
  5. The program stops at the Breakpoint. The yellow line is the next line to be executed.
  6. On the left side appear Variables, Watch and Call Stack. At the top appears the button bar: Continue, Step Over, Step Into, Step Out, Restart, Stop.

Trick: you can also hover the mouse over a variable in the code while stopped, and see its value.

First code example

We will start with a correct program, just to get to know the tool. week-2/debug_intro.py:

def apply_tax(amount):
    tax = amount * 0.17
    return amount + tax

prices = [100, 250, 40]
total = 0

for price in prices:
    total = total + apply_tax(price)

print("Total with tax:", total)

The exercise:

  1. Put a Breakpoint on the line total = total + apply_tax(price).
  2. F5. The program stops. In the Variables window: price = 100, total = 0.
  3. F10 (Step Over). Now total = 117.0, and the yellow line went back to for.
  4. F5 (Continue). We stop again on the same line: price = 250.
  5. This time F11 (Step Into). We are inside apply_tax, and amount = 250. In the Call Stack we see: apply_tax → <module>.
  6. F10 twice: we see tax = 42.5, and then return.
  7. F5 until the end.

Output:

Total with tax: 456.3

Breaking down the code line by line

What we see at each stop, and that is how we understand the flow:

StopYellow lineVariablesExplanation
1total = total + apply_tax(price)price=100, total=0The stop is before the line is executed, so total is still 0.
After F10for price in prices:total=117.0The line was executed in full, including the function.
2total = total + apply_tax(price)price=250, total=117.0Second round.
After F11tax = amount * 0.17amount=250We went into the function. We see only its variables (Local).
After F10return amount + taxamount=250, tax=42.5

Another example

A real bug — from start to finish. A program that calculates a grade average without the lowest grade (as is common in courses: “the lowest grade is not counted”). week-2/drop_lowest.py:

def find_lowest(grades):
    lowest = 0
    for grade in grades:
        if grade < lowest:
            lowest = grade
    return lowest

def average_without_lowest(grades):
    lowest = find_lowest(grades)
    total = sum(grades) - lowest
    return total / (len(grades) - 1)

my_grades = [80, 90, 70, 100]
print("Average without lowest:", average_without_lowest(my_grades))

Expectation: the lowest is 70. That leaves 80, 90, 100, and the average is 90.0.

Actual output:

Average without lowest: 113.33333333333333

An average above 100! This is a Logical Error: no error message, just an impossible result.

Step 1 — Reproduce: the input [80, 90, 70, 100] produces the bug on every run. Excellent, we have a stable case.

Step 2 — Isolate: there are two functions. Which one is guilty? Put a Breakpoint on the line total = sum(grades) - lowest and run with F5. In the Variables window: lowest = 0. But the lowest is 70! The problem is already before this line, inside find_lowest. We isolated it.

Step 3 — Root Cause: stop, put a Breakpoint on lowest = find_lowest(grades), F5, then F11 (Step Into) into find_lowest, and F10 line by line:

Roundgradegrade < lowest?lowest
Start——0
18080 < 0 → False0
29090 < 0 → False0
37070 < 0 → False0
4100100 < 0 → False0

The root cause: lowest starts at 0. No positive grade is smaller than 0, so it is never updated. The assumption “0 is a good starting value” is wrong.

Step 4 — Fix: start from the first grade in the list. It is always a real candidate:

def find_lowest(grades):
    lowest = grades[0]
    for grade in grades:
        if grade < lowest:
            lowest = grade
    return lowest

(Python also has a built-in min(grades), and in real code it is better to use it. Here we wrote it by hand for practice.)

Step 5 — Verify:

Average without lowest: 90.0

And check more cases, not only the original one:

  • [50, 50, 50] → 50.0 ✓
  • [100, 0] → 100.0 ✓ (the lowest, 0, is removed)
  • [70] → crashes! ZeroDivisionError (dividing by len - 1 = 0). We found another bug thanks to the verify step. What to do with a list of one grade? That is a business decision. For example: return the grade itself.

Common mistake

Mistake 1: skipping Reproduce. “The user said it sometimes does not work”, so you change code without a case that reproduces the bug. Without reproducing it, there is no way to know you fixed it.

Mistake 2: debugging with print only, without cleaning up. Temporary prints (print("HERE", x)) are a legitimate, fast tool, but they get forgotten in the code and clutter the output. The Debugger shows all the variables without changing the code. (And in the exceptions and logging topic we will learn Logging, the professional way to leave information in the code.)

Mistake 3: Step Into everything. F11 on print(...) or sum(...) takes you into Python’s internal code, and it is easy to get lost there. Use Step Into on your own functions, and Step Over on built-in functions. (If you went in by mistake, Shift + F11 takes you out.)

Mistake 4: fixing and not checking more cases. As we saw, the Verify step revealed a second bug. Always check edge cases too: an empty list, one item, negative values, zero.

Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

Exercise 1: The five steps

Put the debugging steps in order, and explain in one sentence what you do in each step:

Fix · Reproduce · Verify · Understand · Isolate

Show solution
  1. Reproduce — make the bug happen again, on purpose, with a known input.
  2. Isolate — narrow it down: in which function, on which line, with which value the problem starts.
  3. Understand — understand why it happens, before changing any code.
  4. Fix — fix the root cause, not the symptom.
  5. Verify — run the case that failed again, and also cases that worked before.

Exercise 2: The maximum that came out 0

def find_max(numbers):
    largest = 0
    for number in numbers:
        if number > largest:
            largest = number
    return largest

print(find_max([4, 9, 2]))
print(find_max([-7, -3, -10]))

The second call returns 0, even though 0 is not in the list at all.

  1. Where would you put a Breakpoint, and what would you see in the Variables window?
  2. Fix the bug.

Expected output:

9
-3
Show solution
  1. A Breakpoint on the line if number > largest:. In the Variables window you see that largest stays 0 throughout the loop — no negative number is greater than 0.
  2. Start from the first item of the list, not from 0:
def find_max(numbers):
    largest = numbers[0]
    for number in numbers:
        if number > largest:
            largest = number
    return largest


print(find_max([4, 9, 2]))
print(find_max([-7, -3, -10]))
9
-3

Exercise 3: return inside the loop

def count_vowels(word):
    count = 0
    for letter in word:
        if letter in "aeiou":
            count = count + 1
        return count

print(count_vowels("banana"))

The function returns 0 instead of 3. Use Step Over to follow the loop — what happens after the first letter? Fix it.

Expected output:

3
Show solution

With Step Over you see that after the first letter (b, not a vowel) the run goes straight to return count — inside the loop — and the function ends with 0. The return is indented one level too deep.

def count_vowels(word):
    count = 0
    for letter in word:
        if letter in "aeiou":
            count = count + 1
    return count


print(count_vowels("banana"))
3

Exercise 4: The Debugger’s tools

Explain each term in one sentence, and when you use it:

  1. Breakpoint
  2. Step Over
  3. Step Into
  4. The Variables window
Show solution
  1. Breakpoint — a point where the run stops, so you can check the state at that moment. Put it on the line before the suspicious place.
  2. Step Over — runs the current line and moves to the next one, without going into functions called on it.
  3. Step Into — goes into the function called on the current line, to check what happens inside.
  4. The Variables window — shows the values of all the variables at the current moment. This is where you see where a value differs from what you expected.

Exercise 5: Off-by-one

def sum_to(n):
    total = 0
    for i in range(1, n):
        total = total + i
    return total

print(sum_to(5))

The sum 1+2+3+4+5 should be 15, but 10 comes out. Find the bug and fix it.

Expected output:

15
Show solution

range(1, n) does not include n — the loop runs on 1, 2, 3, 4 only. This is a classic “Off-by-one” bug.

def sum_to(n):
    total = 0
    for i in range(1, n + 1):
        total = total + i
    return total


print(sum_to(5))
15

Summary

  • The five steps: Reproduce → Isolate → Root Cause → Fix → Verify. No guessing.
  • A Breakpoint stops before the line is executed. F5 = Continue, F10 = Step Over, F11 = Step Into, Shift+F11 = Step Out.
  • The Variables window shows the state of the program at the moment of the stop. The Call Stack shows who called whom.
  • You find Logical Errors by comparing the actual values of the variables with the expected values, line by line.
  • After a fix, also check edge cases: empty, one item, zero, negative.

Official documentation