Выброс исключений

Теория

Исключения выбрасывает не только Python — их выбрасывают и ваши функции через raise, когда работу невозможно сделать правильно: количество отрицательное, товара нет, промокод просрочен. Это лучше, чем вернуть «особое значение» (None, -1, строку "ошибка"): особое значение легко не заметить, и оно поедет дальше по программе как обычные данные, пока не сломает что-то далеко от места проблемы. Исключение же нельзя проигнорировать случайно — его нужно либо обработать, либо программа остановится.

Важно решить, где ловить. Функция расчёта обычно не знает, что делать с ошибкой, — она её выбрасывает или пропускает дальше. Ловят на границе: в обработчике запроса, где ошибку можно превратить в понятный ответ пользователю (400 Bad Request) или в запись в лог. Если поймали только чтобы записать в лог, ошибку перевыбрасывают голым raise. А если нужно сменить тип ошибки на более понятный, используют raise НоваяОшибка(...) from e — так сохраняется исходная причина. Свои классы ошибок (OutOfStockError) появятся в разделе ООП.

Наглядно

Таблица 1. Особое значение против исключения.

return None / return "ошибка" raise ValueError("...")
Можно не заметить да — значение похоже на обычные данные нет — без обработки программа остановится
Где проявится проблема далеко от причины ровно там, где случилась
Что знает вызывающий код только «что-то не так» тип ошибки и понятное сообщение
Когда уместно «нет результата» — нормальная ситуация (dict.get) работу нельзя выполнить правильно

Таблица 2. Формы raise.

Запись Что делает Когда
raise ValueError("сообщение") выбрасывает новое исключение данные некорректны, продолжать нельзя
raise (внутри except) перевыбрасывает пойманное исключение как есть поймали, чтобы записать в лог, но обработать не можем
raise ValueError("...") from e новое исключение с сохранённой причиной e переводим низкоуровневую ошибку в понятную

На практике

Задача: проверять количество и наличие товара при оформлении заказа, правильно передавать ошибки наверх и повторять запрос к ненадёжному сервису оплаты.

Шаг 1. Выбрасываем исключение

Функция проверки не может «исправить» отрицательное количество — она сообщает о проблеме:

python
def validate_quantity(qty):
    if qty <= 0:
        raise ValueError(f"количество должно быть больше нуля, получено {qty}")
    return qty

print(validate_quantity(2))
validate_quantity(-1)

Результат:

вывод
2
ValueError: количество должно быть больше нуля, получено -1

Тип ошибки — ValueError: тип данных правильный, значение — нет. Сообщение объясняет, что не так и какое значение пришло.

Шаг 2. Ловим ошибку в вызывающем коде

Обработчик формы знает, что делать: вернуть понятный ответ с кодом 400:

python
def handle_add_to_cart(raw_qty):
    try:
        qty = validate_quantity(int(raw_qty))
    except ValueError as e:
        return {"status": 400, "error": str(e)}
    return {"status": 200, "qty": qty}

print(handle_add_to_cart("2"))
print(handle_add_to_cart("-3"))
print(handle_add_to_cart("много"))

Результат:

вывод
{'status': 200, 'qty': 2}
{'status': 400, 'error': 'количество должно быть больше нуля, получено -3'}
{'status': 400, 'error': "invalid literal for int() with base 10: 'много'"}

Шаг 3. Видим, чем плох возврат «ошибки»

Функция цены возвращает строку вместо ошибки — и проблема всплывает в другом месте:

python
def get_price(catalog, product_id):
    if product_id not in catalog:
        return "нет товара"
    return catalog[product_id]

catalog = {42: 1499}
total = 0
for product_id in (42, 7):
    total += get_price(catalog, product_id)

Результат:

вывод
TypeError: unsupported operand type(s) for +=: 'int' and 'str'

Ошибка — в строке суммирования, хотя причина — отсутствующий товар. В большой программе такую связь ищут долго.

Шаг 4. Перевыбрасываем после записи в лог

Поймали, записали, но обработать не можем — пропускаем дальше голым raise:

python
def charge(amount):
    try:
        return validate_quantity(amount)
    except ValueError:
        print("лог: неверная сумма", amount)
        raise

try:
    charge(0)
except ValueError as e:
    print("обработчик получил:", e)

Результат:

вывод
лог: неверная сумма 0
обработчик получил: количество должно быть больше нуля, получено 0

Шаг 5. Меняем тип ошибки и сохраняем причину

KeyError: 7 ничего не говорит пользователю API. Переводим его в понятную ошибку, сохранив исходную через from:

python
def find_price(catalog, product_id):
    try:
        return catalog[product_id]
    except KeyError as e:
        raise ValueError(f"товар {product_id} не найден") from e

try:
    find_price({42: 1499}, 7)
except ValueError as e:
    print(e, "| причина:", type(e.__cause__).__name__, e.__cause__)

Результат:

вывод
товар 7 не найден | причина: KeyError 7

В traceback такой ошибки видны обе: исходная KeyError и строка «The above exception was the direct cause of the following exception».

Шаг 6. Повторяем запрос через декоратор @retry

Сервис оплаты иногда отвечает сбоем сети (ConnectionError) — такой запрос стоит повторить. А ошибку данных (ValueError) повторять бессмысленно. Имитируем сервис, который падает два раза:

python
import functools

def retry(times, exceptions):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    print(f"попытка {attempt} не удалась: {e}")
                    if attempt == times:
                        raise
        return wrapper
    return decorator

failures = [ConnectionError("таймаут"), ConnectionError("сброс соединения")]

@retry(times=3, exceptions=ConnectionError)
def pay(amount):
    if failures:
        raise failures.pop(0)
    return f"оплачено {amount} ₽"

print(pay(1499))

Результат:

вывод
попытка 1 не удалась: таймаут
попытка 2 не удалась: сброс соединения
оплачено 1499 ₽

Если сбои не закончатся за три попытки, последняя ошибка уйдёт наружу через raise, а не превратится в тихий None. ValueError декоратор не ловит — она сразу дойдёт до вызывающего кода.

Частые ошибки

  • Особое значение вместо исключения. return None или return "ошибка", когда работу нельзя выполнить, — проблема всплывёт далеко от причины.
  • Проглоченная ошибка. except: pass или except ...: return None в глубине кода — программа продолжает работать с неверными данными.
  • raise e вместо raise. Работает, но добавляет в traceback лишнюю строку; для перевыброса как есть пишут голый raise.
  • Перевод ошибки без from. Причина сохраняется неявно, но traceback говорит «During handling … another exception occurred» — будто это новая ошибка обработчика. from e прямо говорит: «вот причина».
  • Повтор при любой ошибке. @retry для Exception повторяет и ошибки данных, которые никогда не пройдут, и тратит время. Повторяйте только временные сбои.

Итог

raise

  • raise ValueError("понятное сообщение") — работу нельзя выполнить правильно.
  • Тип — по смыслу: ValueError, TypeError, KeyError…; сообщение — что не так и какое значение пришло.

Где ловить

  • Низкоуровневые функции выбрасывают или пропускают ошибки.
  • На границе (обработчик запроса) ошибку превращают в ответ или запись в лог.

Передача дальше

  • Голый raise в except — перевыбросить как есть.
  • raise Новая(...) from e — сменить тип, сохранив причину (__cause__).

Повтор

  • @retry(times, exceptions) — только для временных сбоев; после последней попытки — raise.

Вопросы на собеседовании

Почему лучше выбросить исключение, чем вернуть None или код ошибки?

Особое значение легко не проверить: оно смешивается с нормальными данными, и ошибка проявляется далеко от причины. Исключение невозможно проигнорировать случайно, несёт тип и сообщение и останавливает выполнение там, где возникла проблема.

Чем raise отличается от raise e внутри except?

Голый raise перевыбрасывает текущее исключение без изменений. raise e тоже выбросит тот же объект, но добавит в traceback строку, где был raise e. Для простого перевыброса пишут raise.

Что делает raise ... from e?

Создаёт новое исключение и записывает исходное в его __cause__. В traceback показываются обе ошибки с пометкой, что первая — прямая причина второй. Используют, когда переводят низкоуровневую ошибку (KeyError) в ошибку уровня приложения.

Глубже

Как Python хранит цепочки исключений (__context__ и __cause__) и что делает return в finally с летящей ошибкой — в курсе «Python под капотом», тема «Исключения изнутри».

Ошибка в тексте

Сообщить об ошибке могут зарегистрированные пользователи — так в сообщениях меньше спама.

Войти, чтобы сообщить