# Exceptions A: Why Error Handling Is Needed, try and except<no value>

## What will we learn?

- What an **Exception** is.
- Why real software has to **handle** errors and not crash.
- The `try` / `except` syntax.
- **Different** handling for different kinds of errors.

## Why do we need this?

Imagine a small calculator server: one client sends `"hello"` instead of a number, **and the whole server crashes**. All the clients who come after them get "Connection refused".

In the real world:

- A user types "abc" in an age field. The app should not close.
- A file the program tries to read was deleted. It should report it, not crash.
- The internet went down in the middle of a request. It is worth trying again, or showing a message.

Errors **will happen**: bad input, missing files, an unstable network. The only question is what the software **does** when they happen.

## Explanation from the basics

### What is an Exception?

An **Exception** is an object that Python **"throws"** (Raise) when a Runtime Error happens. All the errors we saw yesterday (`ValueError`, `ZeroDivisionError`, `TypeError`...) are Exceptions.

When an Exception is thrown:

1. Python **stops** the current line immediately.
2. It looks for someone who will **"catch"** (Catch) the error.
3. If nobody catches it, **the program crashes** and a Traceback is printed.

`try` / `except` is the way **to catch**.

{{< box "analogy" "Analogy: a safety net in the circus" >}}
A tightrope walker (your code) may fall (an Exception). Without a net, the fall is the end of the show (a crash). **`try`** = "the show is above a net". **`except`** = the net, which catches the fall, and then you decide what to do: try again, tell the audience, or move on to the next act.
{{< /box >}}

### How it works

- **`try:`** — "try to execute this block".
- **`except SomeError:`** — "if a `SomeError` is thrown inside the `try`, **jump here** and execute this block instead of crashing".
- If no error was thrown, the `except` block is **skipped**.
- After the `try`/`except`, the program **continues as usual**.

## Syntax

```python
try:
    # code that might fail
except ErrorType:
    # what to do if ErrorType happened

# several error types, each handled differently
try:
    ...
except ValueError:
    ...
except ZeroDivisionError:
    ...

# get the error object itself (its message)
except ValueError as error:
    print(error)

# same handling for several types
except (ValueError, TypeError):
    ...
```

## First code example

`week-2/safe_age.py`:

```python
text = input("Enter your age: ")

try:
    age = int(text)
    print("Next year you will be", age + 1)
except ValueError:
    print("That is not a whole number:", text)

print("Program continues normally.")
```

**Run 1 (valid input):**

```text
Enter your age: 30
Next year you will be 31
Program continues normally.
```

**Run 2 (invalid input):**

```text
Enter your age: abc
That is not a whole number: abc
Program continues normally.
```

## Breaking down the code line by line

| Line | Run 1 (`30`) | Run 2 (`abc`) |
|---|---|---|
| `text = input(...)` | `text = "30"` | `text = "abc"` |
| `try:` | Enters the block | Enters the block |
| `age = int(text)` | Succeeds: `age = 30` | **A `ValueError` is thrown**, and Python jumps straight to `except` |
| `print("Next year...")` | Printed | **Skipped**. We never reach it |
| `except ValueError:` | Skipped (no error) | **Caught!** |
| `print("That is not...")` | Does not run | Printed |
| `print("Program continues...")` | Printed | Printed, and the program **did not crash** |

**The important point:** the moment an error is thrown, **the rest of the lines in the `try` do not run**. That is why you put inside the `try` only what depends on the success.

## Another example

**A robust division calculator**, with different handling for each kind of error and a loop that asks again. `week-2/safe_divide.py`:

```python
def ask_number(prompt):
    while True:
        text = input(prompt)
        try:
            return float(text)
        except ValueError:
            print("Please type a number, for example 12 or 3.5")

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print("Cannot divide by zero - returning 0")
        return 0

x = ask_number("First number: ")
y = ask_number("Second number: ")
print("Result:", divide(x, y))
```

**Example run:**

```text
First number: ten
Please type a number, for example 12 or 3.5
First number: 10
Second number: 0
Cannot divide by zero - returning 0
Result: 0
```

**Breakdown:**

- `return float(text)` inside a `try` — if the conversion succeeds, the function **returns** immediately and also leaves the loop. If it fails, `except` prints a message, and the loop asks again. That way you "force" the user into valid input.
- Each function catches the error **that is relevant to it**: `ask_number` catches `ValueError` (input), `divide` catches `ZeroDivisionError` (calculation).
- **An open question:** should `divide` return 0 on division by zero? Mathematically, that is a wrong answer! In many cases it is better **not** to catch it here, and let the caller decide. We will talk about it in the lesson on `raise`.

### `as error` — getting the error's details

```python
try:
    number = int("12.5")
except ValueError as error:
    print("Conversion failed:", error)
```

```text
Conversion failed: invalid literal for int() with base 10: '12.5'
```

`error` is the error object. Printing it shows the **message** that would have appeared on the last line of the Traceback.

## Common mistake

**Mistake 1: an empty `except` that catches everything.**

```python
try:
    age = int(input("Age: "))
    print("Double:", age * 2)
    reslt = age / 0
except:
    print("Something went wrong")
```

There are three different problems here (input, division by zero, and even a missing letter in a variable name), and all of them are hidden under "Something went wrong". **It is impossible to debug.** And worst of all: an empty `except:` also catches `KeyboardInterrupt`, so even `Ctrl + C` will not stop a program stuck in a loop like this!

**The rule:** catch **only specific errors that you know how to handle.** All the others continue upwards and crash, and that is **good**, because that is how you discover bugs.

**Mistake 2: a huge `try`.**

```python
try:
    # 40 lines of code...
except ValueError:
    print("Invalid value")
```

Which of the 40 lines failed? Unknown. **Put in the `try` only the lines that may fail with the error you catch**, as few as possible.

**Mistake 3: "swallowing" an error silently.**

```python
try:
    price = float(text)
except ValueError:
    pass
```

`pass` means "do nothing". The error disappears, and `price` may not exist at all, which will cause a confusing `NameError` a few lines later. Always **do something**: a message, an explained default value, or writing to a log.

<!-- 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: Safe conversion to a number

Write a function `to_number(text)` that returns the text as a whole number, and if the text is not a number — returns `None`, without the program crashing.

```python
print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))
```

**Expected output:**

```text
42
None
7
```

{{< solution >}}
```python
def to_number(text):
    try:
        return int(text)
    except ValueError:
        return None


print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))
```

```text
42
None
7
```

`int("hello")` throws a `ValueError`. Without `try`, the program would crash. With `except ValueError` we decide what happens instead.
{{< /solution >}}

### Exercise 2: Different handling for each kind of error

Write a function `safe_divide(a_text, b_text)` that receives two texts, converts them to numbers and divides. It returns:

- the result of the division, if everything is valid;
- `"numbers only"` if one of the texts is not a number;
- `"cannot divide by zero"` if the divisor is 0.

```python
print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))
```

**Expected output:**

```text
2.5
cannot divide by zero
numbers only
```

{{< solution >}}
```python
def safe_divide(a_text, b_text):
    try:
        return int(a_text) / int(b_text)
    except ValueError:
        return "numbers only"
    except ZeroDivisionError:
        return "cannot divide by zero"


print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))
```

```text
2.5
cannot divide by zero
numbers only
```

Each kind of error has its own `except`, with suitable handling. Python checks the `except` blocks in order and enters the first one that matches.
{{< /solution >}}

### Exercise 3: Showing the error message

The following code fails with a `ValueError`. Wrap it in a `try`, catch the error with `except ValueError as error`, and print the text `Could not convert:` followed by the message Python saved in `error`:

```python
number = int("12abc")
```

**Expected output:**

```text
Could not convert: invalid literal for int() with base 10: '12abc'
```

{{< solution >}}
```python
try:
    number = int("12abc")
except ValueError as error:
    print("Could not convert:", error)
```

```text
Could not convert: invalid literal for int() with base 10: '12abc'
```

`as error` saves the error object in a variable. When you print it, you get the message Python created.
{{< /solution >}}

### Exercise 4: Choosing from a list

You have a list of names. Write a function `pick(names, position_text)` that receives a position as text (starting from 1) and returns the name at that position. It returns `"not a number"` for input that is not a number, and `"no such position"` for a position that does not exist.

```python
names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))
```

**Expected output:**

```text
Avi
not a number
no such position
```

{{< solution >}}
```python
def pick(names, position_text):
    try:
        position = int(position_text)
        return names[position - 1]
    except ValueError:
        return "not a number"
    except IndexError:
        return "no such position"


names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))
```

```text
Avi
not a number
no such position
```

Two different errors in two places: `int(...)` may throw a `ValueError`, and accessing the list may throw an `IndexError`.
{{< /solution >}}

### Exercise 5: An except that catches everything

```python
def read_age(text):
    try:
        return int(text)
    except:
        return 0
```

1. Why is `except:` without an error type dangerous?
2. Give an example of a real bug this code would hide.
3. Fix the code.

{{< solution >}}
1. An empty `except:` catches **every** error — including ones we did not think of — and hides them. The code continues as if everything is fine, with a wrong value.
2. If someone passes `None` by mistake instead of text, a `TypeError` is thrown — a real bug in the code that called the function. Instead of seeing it, we silently get age 0. (Even `Ctrl + C` is caught by an empty `except:`.)
3. Catch only the error we expect:

```python
def read_age(text):
    try:
        return int(text)
    except ValueError:
        return 0


print(read_age("30"))
print(read_age("thirty"))
```

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

<!-- exercises:end -->

## Summary

- **Exception** = an error that Python throws at runtime. If nobody catches it, the program crashes.
- `try` = "try". `except SomeError` = "if this error happened, do this instead of crashing".
- The moment an error is thrown, **the rest of the `try` is skipped**.
- `except SomeError as error` gives access to the error message.
- Catch **specific errors** only, with a **small** `try`. No empty `except:` and no `pass` that swallows errors.

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

## Official documentation

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

<!-- docs:end -->
