# Debugging A: What a Bug Is, Kinds of Errors and Reading a Stack Trace<no value>

## What will we learn?

- What a **Bug** is.
- The three kinds of errors: **Syntax Error**, **Runtime Error**, **Logical Error**.
- How to read a **Stack Trace** (Traceback), Python's error message.
- The most common errors, and what each one says.

## Why do we need this?

Developers spend a huge part of their time finding and fixing bugs. The difference between a beginner and a professional is not that the professional has no bugs, but that they **read the error message** and know within seconds where to look. Beginners see a lot of red text and panic. By the end of the lesson, the red text will become a map.

## Explanation from the basics

### What is a Bug?

A **Bug** is **a gap between what the program is supposed to do and what it actually does.** Sometimes the program crashes, and sometimes it "works" but returns a wrong result.

{{< box "tip" "Why is it called a Bug?" >}}
In 1947, engineers of the Mark II computer found a moth (a bug) stuck inside one of the relays, and taped it into the work log with the caption "first actual case of bug being found". The word was used for technical faults even before that, but this story made it famous.
{{< /box >}}

### The three kinds of errors

| Kind | When it is discovered | Does Python report it? | Example |
|---|---|---|---|
| **Syntax Error** | **Before** running (when the syntax is checked) | Yes, and no line runs | A missing `:` or bracket |
| **Runtime Error** (Exception) | **While** running, on the problematic line | Yes, and the lines before it already ran | Division by 0, a variable that does not exist |
| **Logical Error** | **Never** automatically | **No!** The code runs "successfully" | `+` instead of `-`, a reversed condition |

Remember the topic on the computer, the operating system and the network? A Syntax Error is caught when Python translates to Bytecode, before execution. A Runtime Error happens when the PVM executes a specific line and fails.

**A Logical Error is the most dangerous:** no message, no red color, just a wrong result. You find it only if you **check** that the result is correct.

### Stack Trace — the map of the error

When a Runtime Error happens, Python prints a **Traceback** (also called a Stack Trace): the path the code took until the error.

```text
Traceback (most recent call last):
  File "shop.py", line 9, in <module>
    total = checkout(cart)
  File "shop.py", line 5, in checkout
    return subtotal / count
ZeroDivisionError: division by zero
```

**Read it from the bottom up:**

1. **The last line** — the **type** of the error (`ZeroDivisionError`) and the **message** (`division by zero`). Always start here.
2. **The line above it** — **where** exactly: the file `shop.py`, line 5, inside the function `checkout`, and the code itself.
3. **Above that** — **who called** this function: line 9, at the top level of the file (`<module>`).

Why "Stack"? Because every function call is piled onto the **stack** of calls (the Call Stack). The Traceback shows the pile at the moment of the crash, from the first call (at the top) to the last one (at the bottom).

### The common errors in Python

| Error | Meaning | Common cause |
|---|---|---|
| `SyntaxError` | The code is not valid syntax | A missing `:`, brackets, quotes |
| `IndentationError` | Wrong indentation | Mixing spaces, inconsistent indentation |
| `NameError` | A name is not defined | A typo, use before definition |
| `TypeError` | An operation on an unsuitable type | `"5" + 3`, calling a function with the wrong number of arguments |
| `ValueError` | The right type, an invalid value | `int("hello")` |
| `ZeroDivisionError` | Division by zero | A denominator that was not checked |
| `IndexError` | An index outside the list | `items[5]` when there are 3 items |
| `KeyError` | A key that does not exist in a dict | (we will learn it in the data structures and functions topic) |
| `AttributeError` | The object has no such attribute/method | `"abc".push()`, a typo in a method name |
| `FileNotFoundError` | A file does not exist | A wrong path, the Working Directory |

## Syntax

There is no new syntax. The tool is **reading**. The pattern of every Traceback:

```text
Traceback (most recent call last):
  File "<file>", line <N>, in <function or <module>>
    <the code on that line>
  ...                                  ← more frames, each a function call
<ErrorType>: <message>                 ← START READING HERE
```

And of a Syntax Error (without a Traceback, because nothing ran):

```text
  File "<file>", line <N>
    <the code>
        ^                              ← where Python noticed the problem
SyntaxError: <message>
```

## First code example

`week-2/bug_report.py`:

```python
def average(numbers):
    total = sum(numbers)
    return total / len(numbers)

scores = []
print("Average score:", average(scores))
```

**Output:**

```text
Traceback (most recent call last):
  File "C:\dev\kodkod6\week-2\bug_report.py", line 7, in <module>
    print("Average score:", average(scores))
                            ^^^^^^^^^^^^^^^
  File "C:\dev\kodkod6\week-2\bug_report.py", line 3, in average
    return total / len(numbers)
           ~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zero
```

## Breaking down the code line by line

**Reading the Traceback from the bottom up:**

| Line in the output | What it tells us |
|---|---|
| `ZeroDivisionError: division by zero` | **What** happened: division by zero. |
| `line 3, in average` + `return total / len(numbers)` | **Where**: line 3, inside `average`. The `~~~^~~~` marks exactly the division operation. |
| `line 7, in <module>` + `average(scores)` | **Who called**: line 7, at the top level. `<module>` = "not inside a function". |

**Drawing the conclusion:** `len(numbers)` is 0, meaning the list is empty. And who passed an empty list? Line 7 passed `scores`, which is `[]`. **The real bug** is that we did not handle the case of an empty list. The line that crashed (3) is the **symptom**, and deciding what to do with an empty list is the **fix**:

```python
def average(numbers):
    if len(numbers) == 0:
        return 0
    return sum(numbers) / len(numbers)
```

## Another example

**The three kinds of errors on the same function.** The function is supposed to calculate a price after a percentage discount.

**Syntax Error:**

```python
def price_after_discount(price, percent)
    return price - price * percent / 100
```

```text
  File "discount.py", line 1
    def price_after_discount(price, percent)
                                            ^
SyntaxError: expected ':'
```

The colon is missing. Python did not run **a single line**.

**Runtime Error:**

```python
def price_after_discount(price, percent):
    return price - price * percent / 100

print(price_after_discount("100", 10))
```

```text
Traceback (most recent call last):
  File "discount.py", line 4, in <module>
    print(price_after_discount("100", 10))
  File "discount.py", line 2, in price_after_discount
    return price - price * percent / 100
                   ~~~~~~~~~~~~~~~~^~~~~
TypeError: unsupported operand type(s) for /: 'str' and 'int'
```

The syntax is valid, but at runtime `price` is text. `"100" * 10` "works" (it repeats the text 10 times!), but then the division by 100 fails. The `^` points at the `/`.

**Logical Error:**

```python
def price_after_discount(price, percent):
    return price - percent / 100

print(price_after_discount(100, 10))
```

```text
99.9
```

**No error at all!** But a 10% discount on 100 should return `90.0`. We forgot to multiply by `price`. Python cannot know what we **meant**. Only a check against an expected result will reveal it.

## Common mistake

**Mistake 1: reading the Traceback from the top, and panicking.** Always start **from the last line**: the type of the message. Then the line above it. Most of the time, that is all you need.

**Mistake 2: fixing the line that crashed, instead of the cause.** In the first example, a "fix" like `return total / (len(numbers) + 1)` makes the error disappear, but returns a **wrong** result in every case! That turned a Runtime Error into a Logical Error. Even worse.

**Mistake 3: looking for the Syntax Error only on the line shown in the message.** Sometimes the problem is on the **previous** line:

```python
prices = [10, 20, 30
total = sum(prices)
```

```text
  File "test.py", line 1
    prices = [10, 20, 30
             ^
SyntaxError: '[' was never closed
```

Python reached line 2, and discovered that the bracket from line 1 was never closed. In new versions the message is precise, but in general: if the marked line looks fine, check the one before it.

<!-- 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: Which kind of error?

For each snippet — is it a SyntaxError, a Runtime Error or a Logical Error? And what is the problem?

```text
# A
if age > 18
    print("adult")

# B
print(total_price)        # total_price was never defined

# C
def area(width, height):
    return width + height   # should be the area of a rectangle
```

{{< solution >}}
- **A — SyntaxError**: the colon `:` is missing at the end of the `if` line. No line runs.
- **B — Runtime Error** (`NameError`): the syntax is valid, but at runtime Python cannot find a variable named `total_price`.
- **C — Logical Error**: there is no error at all, but the result is wrong — an area is calculated by multiplying, `width * height`. You find this error only if you check the result.
{{< /solution >}}

### Exercise 2: Reading a Stack Trace

```text
Traceback (most recent call last):
  File "shop.py", line 12, in <module>
    print(checkout([20, 30], 0))
  File "shop.py", line 7, in checkout
    return average_price(prices, count)
  File "shop.py", line 3, in average_price
    return sum(prices) / count
ZeroDivisionError: division by zero
```

1. What is the type of the error and what is the message?
2. In which file, on which line and in which function did it happen?
3. Where did the problematic value come from?

{{< solution >}}
1. **`ZeroDivisionError: division by zero`** — division by zero. Read the **last** line first.
2. In the file `shop.py`, **line 3**, in the function `average_price` — that is the lowest line in the list.
3. Read upwards: `average_price` was called from line 7 in `checkout`, and `checkout` was called from line 12 with `count = 0`. So the problem started **in the call on line 12**, which passed 0.
{{< /solution >}}

### Exercise 3: A logical bug in a temperature conversion

```python
def to_celsius(fahrenheit):
    return fahrenheit - 32 * 5 / 9

print(to_celsius(212))
print(to_celsius(32))
```

The results should be 100 and 0, but something else comes out. Find the bug and fix it.

**Expected output:**

```text
100.0
0.0
```

{{< solution >}}
```python
def to_celsius(fahrenheit):
    return (fahrenheit - 32) * 5 / 9


print(to_celsius(212))
print(to_celsius(32))
```

```text
100.0
0.0
```

Order of operations: multiplication and division come before subtraction, so the original code calculated `fahrenheit - (32 * 5 / 9)`. The brackets force the subtraction to happen first. This is a **Logical Error** — no error message, just a wrong result.
{{< /solution >}}

### Exercise 4: TypeError

```python
age = 25
print("Age: " + age)
```

1. Which error will you get, and why?
2. Fix it in two different ways.

**Expected output:**

```text
Age: 25
```

{{< solution >}}
1. **`TypeError: can only concatenate str (not "int") to str`** — you cannot join text and a number with `+`.
2. Two ways:

```python
age = 25
print("Age: " + str(age))
```

```text
Age: 25
```

```python
age = 25
print("Age:", age)
```

```text
Age: 25
```

The first converts the number to text. The second passes two values to `print`, and it adds a space between them.
{{< /solution >}}

### Exercise 5: The average of an empty list

```python
def average(numbers):
    return sum(numbers) / len(numbers)

print(average([80, 90]))
print(average([]))
```

1. What will happen in the second call, and which error will you get?
2. Fix the function so that for an empty list it returns 0.

**Expected output:**

```text
85.0
0
```

{{< solution >}}
1. `len([])` is 0, so you get **`ZeroDivisionError: division by zero`** — a runtime error.
2. Check this case **before** the division:

```python
def average(numbers):
    if len(numbers) == 0:
        return 0
    return sum(numbers) / len(numbers)


print(average([80, 90]))
print(average([]))
```

```text
85.0
0
```
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **Bug** = a gap between the desired behavior and the actual behavior.
- **Syntax Error** — before running, no line runs. **Runtime Error** — in the middle of running, with a Traceback. **Logical Error** — no message, just a wrong result.
- **Read a Traceback from the bottom up:** type + message → line and function → who called.
- The line that crashed is sometimes only the **symptom**. Look for the **cause**.
- Logical Errors are discovered only when you compare a result with an **expected value**.

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

## Official documentation

- [Python Tutorial — Syntax Errors](https://docs.python.org/3/tutorial/errors.html#syntax-errors)
- [Python Tutorial — Exceptions](https://docs.python.org/3/tutorial/errors.html#exceptions)
- [Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#concrete-exceptions)

<!-- docs:end -->
