# Debugging B: The Debugging Process and the VS Code Debugger<no value>

## 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

| Step | Name | The question it answers |
|---|---|---|
| 1 | **Reproduce** | How do you make the bug happen **every time**? With which input? |
| 2 | **Isolate** | **Where** in the code is the problem? Narrow it down to a line or a function. |
| 3 | **Root Cause** | **Why** does it happen? Not the symptom, but the cause. |
| 4 | **Fix** | Fix the **cause**, and do not hide the symptom. |
| 5 | **Verify** | Is the bug gone? **And did nothing else break?** |

{{< box "analogy" "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.
{{< /box >}}

### 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:**

| Term | What it is |
|---|---|
| **Breakpoint** | A "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. |
| **Variables** | A window that shows all the variables and their values **at this moment**. |
| **Watch** | Expressions you chose to follow, for example `total / count`. |
| **Call Stack** | The chain of calls: which function called which. It is the Traceback, only "live". |
| **Stop** (`Shift+F5`) | Stop debugging. |

{{< box "analogy" "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.
{{< /box >}}

## 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`:

```python
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:**

```text
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:

| Stop | Yellow line | Variables | Explanation |
|---|---|---|---|
| 1 | `total = total + apply_tax(price)` | `price=100, total=0` | The stop is **before** the line is executed, so total is still 0. |
| After `F10` | `for price in prices:` | `total=117.0` | The line was executed in full, including the function. |
| 2 | `total = total + apply_tax(price)` | `price=250, total=117.0` | Second round. |
| After `F11` | `tax = amount * 0.17` | `amount=250` | We went into the function. We see only **its** variables (Local). |
| After `F10` | `return amount + tax` | `amount=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`:

```python
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:**

```text
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:

| Round | `grade` | `grade < lowest`? | `lowest` |
|---|---|---|---|
| Start | — | — | 0 |
| 1 | 80 | `80 < 0` → False | 0 |
| 2 | 90 | `90 < 0` → False | 0 |
| 3 | 70 | `70 < 0` → False | 0 |
| 4 | 100 | `100 < 0` → False | 0 |

**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:

```python
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:**

```text
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.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## 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

{{< 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.
{{< /solution >}}

### Exercise 2: The maximum that came out 0

```python
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:**

```text
9
-3
```

{{< 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:

```python
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]))
```

```text
9
-3
```
{{< /solution >}}

### Exercise 3: return inside the loop

```python
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:**

```text
3
```

{{< 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.

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


print(count_vowels("banana"))
```

```text
3
```
{{< /solution >}}

### 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

{{< 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.
{{< /solution >}}

### Exercise 5: Off-by-one

```python
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:**

```text
15
```

{{< 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.

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


print(sum_to(5))
```

```text
15
```
{{< /solution >}}

<!-- exercises:end -->

## 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.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Official documentation

- [VS Code — Python debugging](https://code.visualstudio.com/docs/python/debugging)
- [VS Code — Debugging: Breakpoints](https://code.visualstudio.com/docs/debugtest/debugging#_breakpoints)

<!-- docs:end -->
