# Исключения C: raise — выбрасываем ошибку сами<no value>

## Что мы изучим?

До сих пор ошибки выбрасывал **Python**, а мы их **ловили**. Сегодня научимся выбрасывать их сами — с помощью **`raise`**, когда наш код обнаруживает недопустимое состояние.

## Зачем это нужно?

Допустим, вы написали функцию `withdraw(balance, amount)` для снятия денег с банковского счёта. Что она должна делать, когда её просят снять **минус 500**? Варианты:

1. **Молча выполнить** — баланс **вырастет** на 500. Серьёзный денежный баг.
2. **Напечатать «ошибка»** — но тот, кто вызвал функцию, не знает, что она не сработала, и продолжает работать с неверным результатом.
3. **Вернуть специальное значение** (например, `-1` или `None`) — вызывающий код должен не забыть его проверить. Если забудет, баг «просочится».
4. **Выбросить ошибку** — вызывающий код **обязан** с ней разобраться, иначе программа остановится с понятным сообщением ровно в проблемном месте.

Вариант 4 — профессиональный способ. В теме ООП мы будем использовать его для **проверки данных** (Validation) внутри классов, а в темах бэкенда и баз данных сервер FastAPI будет превращать такие ошибки в HTTP-ответ `400` или `404`.

## Объяснение с основ

### `raise`

`raise` **выбрасывает** исключение — точно так же, как это делает Python, когда что-то ломается. С момента `raise`:

- Функция **немедленно останавливается**. Ни одна строка после неё не выполняется, и значение не возвращается.
- Ошибка «поднимается» к тому, кто вызвал функцию. Если он ловит её через `except` — отлично. Если нет, она поднимается дальше, и в конце концов программа падает с Traceback.

### Правильное разделение: кто обнаруживает и кто обрабатывает

| Роль | Кто | Что делает |
|---|---|---|
| **Обнаруживает** проблему | Функция, которая получает данные | Проверяет и **выбрасывает** (`raise`) |
| **Решает**, что делать | Тот, кто вызвал функцию | **Ловит** (`except`) и решает: сообщение, ещё одна попытка, значение по умолчанию |

{{< box "analogy" "Аналогия: склад ресторана" >}}
Официант (вызывающий код) просит кладовщика (функцию) 5 помидоров. Помидоров нет. Хороший кладовщик **не** приносит молча 5 яблок (неверное значение) и не «решает» сам поменять блюдо. Он **объявляет** (`raise`): «Помидоров нет!». А официант, который знает клиента, решает: предложить другое блюдо или извиниться. Каждый отвечает за свою область.
{{< /box >}}

### Какой тип ошибки выбрасывать?

Используйте встроенные типы по смыслу:

| Тип | Когда |
|---|---|
| `ValueError` | Тип правильный, но **значение** недопустимо: отрицательный возраст, нулевая сумма, email без `@` |
| `TypeError` | Неверный **тип**: получен текст вместо числа |
| `KeyError` / `LookupError` | Ищем то, чего не существует |
| `PermissionError` | Нет права на действие |

Сообщение должно объяснять, **что не так и почему**: `"amount must be positive, got -500"`, а не `"error"`.

(В теме ООП, после того как изучим классы, увидим, как создавать **собственные типы ошибок**, например `InsufficientFundsError`.)

## Синтаксис

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

## Первый пример кода

`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")
```

**Вывод:**

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

## Разбор кода построчно

| Строка | Объяснение |
|---|---|
| `if amount <= 0:` | **Проверка данных** (Validation): проверить, прежде чем что-либо делать. |
| `raise ValueError("...")` | Создать объект `ValueError` с сообщением и выбросить его. Функция останавливается здесь. |
| `if amount > balance:` | Вторая проверка — для другого бизнес-правила. |
| `return balance - amount` | Сюда мы попадаем, **только** если все проверки пройдены. Дальше данные безопасны. |
| `withdraw(1000, 300)` | Проходит проверки и возвращает 700. |
| `withdraw(1000, -500)` | Выбрасывается ошибка. Никто её не ловит, поэтому программа падает с понятным Traceback. Последняя строка кода не выполняется. |

**Обратите внимание на качество Traceback:** сообщение говорит точно, **в чём** проблема (`amount must be positive`) и **какое значение** её вызвало (`-500`). Так выглядит хорошая ошибка.

## Ещё один пример

**Решает вызывающий код.** Та же функция с кодом, который ловит и обрабатывает. `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)
```

**Вывод:**

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

**Разбор:**

- `withdraw` ничего не печатает и не знает, что будет дальше. Она только **обнаруживает** и **выбрасывает**.
- Цикл (вызывающий код) **решает**: запрос отклоняется с сообщением, и цикл переходит к следующему запросу.
- Когда ошибка выброшена, присваивание `balance = withdraw(...)` **не выполняется**, поэтому баланс остаётся прежним. Именно этого мы и хотели.
- Завтра ту же `withdraw` можно использовать на сайте, в приложении или на сервере, и каждый из них обработает ошибку по-своему (сообщение на экране, HTTP-ответ `400`...).

### `raise` без ничего после — передаём дальше

Иногда при ошибке нужно сделать **что-то** (например, записать её), но **не** «заставить её исчезнуть»:

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

Одиночный `raise` внутри `except` снова выбрасывает **ту же самую ошибку**. Она продолжает подниматься к тому, кто выше.

## Типичная ошибка

**Ошибка 1: `print` вместо `raise`.**

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

Функция **печатает** сообщение, но **продолжает** и возвращает отрицательный баланс! Вызывающий код не знает, что что-то пошло не так. К тому же печать «застревает» на экране и ничего не значит на сервере или в приложении.

**Ошибка 2: бесполезное сообщение.**

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

Какой ввод? Какое было значение? Что ожидается? Хорошее сообщение = **каково условие** + **что получено**: `"age must be between 0 and 120, got 150"`.

**Ошибка 3: выбрасывать самый общий тип.**

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

`Exception` — родитель почти всех ошибок. Тому, кто хочет поймать **только** эту проблему, понадобится `except Exception`, который ловит и посторонние баги. Выбирайте конкретный тип (`ValueError`).

**Ошибка 4: поймать и выбросить, потеряв контекст.**

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

Исходное сообщение (с деталями) потеряно. Если хотите передать ошибку дальше, одиночный `raise` сохраняет исходную ошибку.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

### Упражнение 1: Допустимый возраст

Напишите функцию `set_age(age)`, которая возвращает возраст, если он от 0 до 120, а иначе **выбрасывает** `ValueError` с сообщением, показанным в ожидаемом выводе (включая полученный возраст). Затем напишите цикл, который пробует возрасты `[25, -3, 150]`: для допустимого возраста печатает `ok:` и возраст, а при ошибке — `error:` и сообщение.

**Ожидаемый вывод:**

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

Функция только **обнаруживает** проблему и выбрасывает ошибку. Вызывающий её код — цикл — **решает**, что делать.
{{< /solution >}}

### Упражнение 2: Имя пользователя

Напишите функцию `validate_username(name)`, которая выбрасывает `ValueError`:

- если имя короче 3 символов — с сообщением `too short`;
- если в нём есть пробел — с сообщением `no spaces allowed`.

Если имя допустимо — возвращает его.

```python
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 >}}
```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 >}}

### Упражнение 3: Решает вызывающий код

Следующая функция выбрасывает ошибку для отрицательной цены:

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

Напишите две функции, которые используют её двумя разными способами:

1. `report_price(price)` — печатает цену после скидки 10% или `invalid price`, если была ошибка.
2. `price_or_zero(price)` — возвращает цену после скидки 10% или 0, если была ошибка.

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

**Ожидаемый вывод:**

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

Одна и та же функция, два разных способа обработки. Поэтому `apply_discount` **выбрасывает** ошибку, а не печатает сама: она не знает, что нужно вызывающему коду.
{{< /solution >}}

### Упражнение 4: return None или raise

Две версии одной функции:

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

Кто-то написал: `balance = withdraw(100, 500)`. Что произойдёт в каждой версии и почему B лучше?

{{< solution >}}
- **A**: печатается «Not enough money!», а `balance` получает **`None`**. Программа продолжает работать с балансом `None` и упадёт позже где-то в другом месте — далеко от настоящей проблемы.
- **B**: `ValueError` выбрасывается **ровно в месте** проблемы. Если вызывающий код его не обработал, Traceback указывает прямо на проблемное снятие. Если обработал — он сам решает, что показать.
- Кроме того, B не печатает сама: возможно, вызывающий код — это сервер, которому нужно вернуть сообщение, а не печатать на экран.
{{< /solution >}}

### Упражнение 5: Преобразование оценки

Напишите функцию `parse_grade(text)`, которая преобразует текст в оценку. Она выбрасывает `ValueError`, если текст не число (это уже происходит в `int`), или если оценка вне диапазона от 0 до 100 — с сообщением, показанным в ожидаемом выводе (включая полученную оценку).

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

{{< 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` не ловит ошибку из `int` — она позволяет ей пройти дальше к вызывающему коду, точно так же, как ошибке, которую выбрасывает сама.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

- `raise SomeError("message")` выбрасывает ошибку. Функция немедленно останавливается, и значение не возвращается.
- **Функция обнаруживает и выбрасывает. Вызывающий код ловит и решает.**
- `ValueError` для недопустимого значения, `TypeError` для неверного типа. Не общий `Exception`.
- Хорошее сообщение = каково правило + что получено.
- Одиночный `raise` внутри `except` передаёт **ту же** ошибку дальше.
- Выбросить ошибку лучше, чем напечатать «ошибка» или вернуть специальное значение, потому что ошибку нельзя «забыть проверить».

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

## Официальная документация

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