# Exceptions C: raise — Throwing an Error Ourselves<no value>

## What will we learn?

Until now **Python** threw errors, and we **caught** them. Today we will learn to throw them ourselves, with **`raise`**, when our code detects an invalid state.

## Why do we need this?

Suppose you wrote a function `withdraw(balance, amount)` for withdrawing from a bank account. What should it do when asked to withdraw **minus 500**? Options:

1. **Do it silently** — the balance **grows** by 500. A serious money bug.
2. **Print "error"** — but whoever called the function does not know it failed, and keeps working with a wrong result.
3. **Return a special value** (like `-1` or `None`) — the caller must remember to check it. If they forget, the bug "leaks".
4. **Throw an error** — the caller **must** deal with it, or the program stops with a clear message exactly at the problematic place.

Option 4 is the professional way. In the OOP topic we will use it for **Validation** inside classes, and in the backend and databases topics a FastAPI server will turn errors like these into an HTTP `400` or `404` response.

## Explanation from the basics

### `raise`

`raise` **throws** an Exception, exactly as Python does when something breaks. From the moment of the `raise`:

- The function **stops immediately**. No line after it runs, and no value is returned.
- The error "rises" to whoever called the function. If they catch it with `except`, excellent. If not, it keeps rising, and in the end the program crashes with a Traceback.

### The right division: who detects and who handles

| Role | Who | What they do |
|---|---|---|
| **Detects** the problem | The function that receives the data | Checks and **throws** (`raise`) |
| **Decides** what to do | Whoever called the function | **Catches** (`except`) and decides: a message, another attempt, a default |

{{< box "analogy" "Analogy: a restaurant storeroom" >}}
The waiter (the caller) asks the storekeeper (the function) for 5 tomatoes. There are no tomatoes. A good storekeeper does **not** silently bring 5 apples (a wrong value), and does not "decide" on their own to change the dish. They **announce** (`raise`): "No tomatoes!". And the waiter, who knows the customer, decides: offer another dish or apologize. Each one is responsible for their own area.
{{< /box >}}

### Which error type to throw?

Use the built-in types, by meaning:

| Type | When |
|---|---|
| `ValueError` | The type is right, but the **value** is invalid: a negative age, a zero amount, an email without `@` |
| `TypeError` | The **type** is wrong: text was received instead of a number |
| `KeyError` / `LookupError` | Looking for something that does not exist |
| `PermissionError` | No permission for the action |

The message should explain **what is wrong and why**: `"amount must be positive, got -500"`, not `"error"`.

(In the OOP topic, after we learn classes, we will see how to create **our own error types**, like `InsufficientFundsError`.)

## Syntax

```python
raise ValueError("clear message about what is wrong")

# typical pattern: validate at the start of a function
def function_name(value):
    if not is_valid(value):
        raise ValueError("why it is invalid")
    # ... normal work, only with valid data

# the caller decides
try:
    function_name(bad_value)
except ValueError as error:
    print("Problem:", error)

# re-raise: handle partly, then pass the same error up
except ValueError:
    print("logging the problem...")
    raise
```

## First code example

`week-2/withdraw.py`:

```python
def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("amount must be positive, got " + str(amount))
    if amount > balance:
        raise ValueError("insufficient funds: balance " + str(balance) + ", requested " + str(amount))
    return balance - amount

print(withdraw(1000, 300))
print(withdraw(1000, -500))
print("this line never runs")
```

**Output:**

```text
700
Traceback (most recent call last):
  File "withdraw.py", line 10, in <module>
    print(withdraw(1000, -500))
  File "withdraw.py", line 3, in withdraw
    raise ValueError("amount must be positive, got " + str(amount))
ValueError: amount must be positive, got -500
```

## Breaking down the code line by line

| Line | Explanation |
|---|---|
| `if amount <= 0:` | **Validation** (checking the input): check before doing anything. |
| `raise ValueError("...")` | Create a `ValueError` object with a message, and throw it. The function stops here. |
| `if amount > balance:` | A second check, for another business rule. |
| `return balance - amount` | We get here **only** if all the checks passed. From here on, the data is safe. |
| `withdraw(1000, 300)` | Passes the checks and returns 700. |
| `withdraw(1000, -500)` | An error is thrown. Nobody catches it, so the program crashes with a clear Traceback. The last line of code does not run. |

**Notice the quality of the Traceback:** the message says exactly **what** the problem is (`amount must be positive`) and **which value** caused it (`-500`). That is what a good error looks like.

## Another example

**The caller decides.** The same function, with code that catches and handles. `week-2/atm_safe.py`:

```python
def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("amount must be positive, got " + str(amount))
    if amount > balance:
        raise ValueError("insufficient funds: balance " + str(balance) + ", requested " + str(amount))
    return balance - amount

balance = 1000
requests = [200, -50, 5000, 300]

for amount in requests:
    try:
        balance = withdraw(balance, amount)
    except ValueError as error:
        print("Rejected:", error)
    else:
        print("Withdrew", amount, "- new balance:", balance)

print("Final balance:", balance)
```

**Output:**

```text
Withdrew 200 - new balance: 800
Rejected: amount must be positive, got -50
Rejected: insufficient funds: balance 800, requested 5000
Withdrew 300 - new balance: 500
Final balance: 500
```

**Breakdown:**

- `withdraw` prints nothing and does not know what will happen next. It only **detects** and **throws**.
- The loop (the caller) **decides**: the request is rejected with a message, and the loop continues to the next request.
- When the error is thrown, the assignment `balance = withdraw(...)` **is not executed**, so the balance stays as it was. That is exactly what we wanted.
- Tomorrow the same `withdraw` can be used on a website, in an app or in a server, and each of them will handle the error its own way (a message on the screen, an HTTP `400` response...).

### `raise` with nothing after it — passing it on

Sometimes you want to do **something** when there is an error (for example, record it), but **not** "make it disappear":

```python
try:
    balance = withdraw(balance, 5000)
except ValueError:
    print("Audit: a failed withdrawal attempt was recorded")
    raise
```

`raise` alone, inside `except`, throws **exactly the same error again**. It keeps rising to whoever is above.

## Common mistake

**Mistake 1: `print` instead of `raise`.**

```python
def withdraw(balance, amount):
    if amount > balance:
        print("Not enough money!")
    return balance - amount
```

The function **prints** a message, but **continues** and returns a negative balance! The caller does not know something went wrong. The print is also "stuck" on the screen, and means nothing in a server or an app.

**Mistake 2: an unhelpful message.**

```python
raise ValueError("error")
raise ValueError("bad input")
```

Which input? What was the value? What is expected? A good message = **what the condition is** + **what was received**: `"age must be between 0 and 120, got 150"`.

**Mistake 3: throwing the most general type.**

```python
raise Exception("amount must be positive")
```

`Exception` is the parent of almost all errors. Whoever wants to catch **only** this problem will need `except Exception`, which also catches unrelated bugs. Choose a specific type (`ValueError`).

**Mistake 4: catching and throwing, and losing the context.**

```python
except ValueError:
    raise ValueError("something failed")
```

The original message (with the details) was lost. If you want to pass it on, `raise` alone keeps the original error.

<!-- 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: A valid age

Write a function `set_age(age)` that returns the age if it is between 0 and 120, and otherwise **throws** a `ValueError` with the message shown in the expected output (including the age that was received). Then write a loop that tries the ages `[25, -3, 150]`: for a valid age it prints `ok:` and the age, and for an error — `error:` and the message.

**Expected output:**

```text
ok: 25
error: age must be between 0 and 120, got -3
error: age must be between 0 and 120, got 150
```

{{< solution >}}
```python
MIN_AGE = 0
MAX_AGE = 120


def set_age(age):
    if age < MIN_AGE or age > MAX_AGE:
        raise ValueError("age must be between 0 and 120, got " + str(age))
    return age


for age in [25, -3, 150]:
    try:
        print("ok:", set_age(age))
    except ValueError as error:
        print("error:", error)
```

```text
ok: 25
error: age must be between 0 and 120, got -3
error: age must be between 0 and 120, got 150
```

The function only **detects** the problem and throws. Its caller — the loop — **decides** what to do.
{{< /solution >}}

### Exercise 2: A username

Write a function `validate_username(name)` that throws a `ValueError`:

- if the name is shorter than 3 characters — with the message `too short`;
- if it contains a space — with the message `no spaces allowed`.

If the name is valid — it returns it.

```python
for name in ["dana", "al", "big boss"]:
    try:
        print("valid:", validate_username(name))
    except ValueError as error:
        print(name, "->", error)
```

**Expected output:**

```text
valid: dana
al -> too short
big boss -> no spaces allowed
```

{{< solution >}}
```python
MIN_USERNAME_LENGTH = 3


def validate_username(name):
    if len(name) < MIN_USERNAME_LENGTH:
        raise ValueError("too short")
    if " " in name:
        raise ValueError("no spaces allowed")
    return name


for name in ["dana", "al", "big boss"]:
    try:
        print("valid:", validate_username(name))
    except ValueError as error:
        print(name, "->", error)
```

```text
valid: dana
al -> too short
big boss -> no spaces allowed
```
{{< /solution >}}

### Exercise 3: The caller decides

The following function throws an error for a negative price:

```python
def apply_discount(price, percent):
    if price < 0:
        raise ValueError("price cannot be negative")
    return price - price * percent / 100
```

Write two functions that use it in two different ways:

1. `report_price(price)` — prints the price after a 10% discount, or `invalid price` if there was an error.
2. `price_or_zero(price)` — returns the price after a 10% discount, or 0 if there was an error.

```python
report_price(200)
report_price(-5)
print(price_or_zero(50))
print(price_or_zero(-5))
```

**Expected output:**

```text
180.0
invalid price
45.0
0
```

{{< solution >}}
```python
def apply_discount(price, percent):
    if price < 0:
        raise ValueError("price cannot be negative")
    return price - price * percent / 100


def report_price(price):
    try:
        print(apply_discount(price, 10))
    except ValueError:
        print("invalid price")


def price_or_zero(price):
    try:
        return apply_discount(price, 10)
    except ValueError:
        return 0


report_price(200)
report_price(-5)
print(price_or_zero(50))
print(price_or_zero(-5))
```

```text
180.0
invalid price
45.0
0
```

The same function, two different ways of handling it. That is why `apply_discount` **throws** and does not print by itself: it does not know what the caller needs.
{{< /solution >}}

### Exercise 4: return None versus raise

Two versions of the same function:

```text
# A                                   # B
def withdraw(balance, amount):        def withdraw(balance, amount):
    if amount > balance:                  if amount > balance:
        print("Not enough money!")            raise ValueError("not enough money")
        return None                       return balance - amount
    return balance - amount
```

Someone wrote: `balance = withdraw(100, 500)`. What will happen in each version, and why is B better?

{{< solution >}}
- **A**: "Not enough money!" is printed, and `balance` gets **`None`**. The program continues with a balance of `None`, and will crash later somewhere else — far from the real problem.
- **B**: a `ValueError` is thrown **exactly at the place** of the problem. If the caller did not handle it, the Traceback points straight at the problematic withdrawal. If they did — they decide for themselves what to show.
- B also does not print by itself: maybe the caller is a server that needs to return a message, not print to the screen.
{{< /solution >}}

### Exercise 5: Converting a grade

Write a function `parse_grade(text)` that converts text to a grade. It throws a `ValueError` if the text is not a number (that already happens in `int`), or if the grade is outside the range 0 to 100 — with the message shown in the expected output (including the grade that was received).

```python
for text in ["87", "abc", "120"]:
    try:
        print("grade:", parse_grade(text))
    except ValueError as error:
        print("rejected:", error)
```

**Expected output:**

```text
grade: 87
rejected: invalid literal for int() with base 10: 'abc'
rejected: grade must be 0-100, got 120
```

{{< solution >}}
```python
def parse_grade(text):
    grade = int(text)
    if grade < 0 or grade > 100:
        raise ValueError("grade must be 0-100, got " + str(grade))
    return grade


for text in ["87", "abc", "120"]:
    try:
        print("grade:", parse_grade(text))
    except ValueError as error:
        print("rejected:", error)
```

```text
grade: 87
rejected: invalid literal for int() with base 10: 'abc'
rejected: grade must be 0-100, got 120
```

`parse_grade` does not catch the error from `int` — it lets it pass on to the caller, exactly like the error it throws itself.
{{< /solution >}}

<!-- exercises:end -->

## Summary

- `raise SomeError("message")` throws an error. The function stops immediately, and no value is returned.
- **The function detects and throws. The caller catches and decides.**
- `ValueError` for an invalid value, `TypeError` for a wrong type. Not a general `Exception`.
- A good message = what the rule is + what was received.
- `raise` alone inside `except` passes **the same** error on.
- Throwing is better than printing "error" or returning a special value, because you cannot "forget to check" an error.

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

## Official documentation

- [Python Tutorial — Raising Exceptions](https://docs.python.org/3/tutorial/errors.html#raising-exceptions)
- [The raise statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement)

<!-- docs:end -->
