Django-Admin.ru

try/except в Python: обработка исключений

Как работает try/except в Python: else и finally, несколько исключений, as e, raise и raise from, свои исключения, contextlib.suppress и except* с примерами.

Редакция Django-Admin.ru
· обновлено · 9 мин чтения

Содержание · 10 разделов
  1. Синтаксис try/except/else/finally
  2. Зачем нужен else
  3. Зачем нужен finally
  4. Как перехватить несколько исключений
  5. as e: доступ к объекту исключения
  6. raise: выбросить и перевыбросить исключение
  7. raise from: цепочка исключений
  8. Свои исключения
  9. contextlib.suppress: игнорировать ожидаемую ошибку
  10. ExceptionGroup и except* (Python 3.11+)
  11. Чего не делать
  12. Голый except
  13. Проглатывание ошибок
  14. Слишком широкий try
  15. Исключения вместо простой проверки
  16. Частые вопросы
  17. Можно ли использовать try без except?
  18. Как вывести полный traceback в except?
  19. Что лучше: return в finally или нет?
  20. Как поймать ModuleNotFoundError?
  21. Как повторять ввод, пока пользователь не введёт правильное значение?
  22. Замедляет ли try/except программу?
  23. Итог

Конструкция try/except в Python перехватывает исключения: код, который может упасть, помещают в блок try, а реакцию на ошибку — в блок except. Если исключение произошло и подходит под указанный тип, программа не завершается, а выполняет обработчик.

try:
    age = int(input("Возраст: "))
except ValueError:
    print("Введите число")

Если пользователь введёт abc, int() выбросит ValueError, и вместо traceback программа напечатает Введите число. Ниже — полный синтаксис с else и finally, перехват нескольких исключений, raise from, собственные классы исключений и ошибки, которых стоит избегать. Примеры проверены на Python 3.12.

Синтаксис try/except/else/finally

Полная форма содержит четыре блока, обязательны только try и хотя бы один except или finally:

try:
    ...   # код, который может выбросить исключение
except ValueError:
    ...   # выполняется, если в try возник ValueError
else:
    ...   # выполняется, если в try исключений не было
finally:
    ...   # выполняется всегда: и после ошибки, и без неё

Порядок выполнения на примере:

def parse_age(raw):
    try:
        age = int(raw)
    except ValueError:
        print(f"Не число: {raw!r}")
        return None
    else:
        print("Преобразовано без ошибок")
        return age
    finally:
        print("finally выполняется всегда")


print(parse_age("42"))
print(parse_age("сорок"))
Преобразовано без ошибок
finally выполняется всегда
42
Не число: 'сорок'
finally выполняется всегда
None

finally срабатывает даже тогда, когда из try, except или else уже выполняется return, — перед фактическим выходом из функции.

Зачем нужен else

В try стоит держать только ту строку, которая может выбросить ожидаемое исключение. Остальной код «при успехе» выносят в else: так except случайно не перехватит ошибку из кода, который вы не собирались защищать.

Зачем нужен finally

finally нужен для освобождения ресурсов: закрыть файл или соединение, снять блокировку, удалить временные данные. Для файлов и многих других ресурсов удобнее контекстный менеджер with — он делает то же самое автоматически:

with open("data.txt", encoding="utf-8") as f:
    text = f.read()
# файл закрыт, даже если внутри with возникло исключение

Как перехватить несколько исключений

Несколько типов с одинаковой обработкой перечисляют кортежем:

def safe_div(a, b):
    try:
        return a / b
    except (ZeroDivisionError, TypeError) as e:
        print(f"{type(e).__name__}: {e}")
        return None


safe_div(1, 0)
safe_div(1, "2")
ZeroDivisionError: division by zero
TypeError: unsupported operand type(s) for /: 'int' and 'str'

С Python 3.14 (PEP 758) скобки можно не писать, если не нужен as: except ZeroDivisionError, TypeError:. С as e кортеж в скобках по-прежнему обязателен.

Если обработка разная — пишут несколько блоков except. Python проверяет их сверху вниз и выполняет первый подходящий, поэтому более конкретные исключения ставят выше общих:

import json

try:
    with open("config.json", encoding="utf-8") as f:
        config = json.load(f)
except FileNotFoundError:
    config = {}
except json.JSONDecodeError as e:
    print(f"Ошибка в config.json: {e}")
    raise

except перехватывает и все подклассы указанного исключения: except LookupError поймает и KeyError, и IndexError. Иерархия встроенных исключений показана в руководстве «Ошибки в Python: как читать traceback».

as e: доступ к объекту исключения

Конструкция except Тип as e сохраняет исключение в переменную. У него есть сообщение, тип и аргументы:

try:
    int("x")
except ValueError as e:
    print(e)               # → invalid literal for int() with base 10: 'x'
    print(type(e).__name__) # → ValueError
    print(e.args)          # → ("invalid literal for int() with base 10: 'x'",)

Переменная e существует только внутри блока except — после него Python её удаляет:

try:
    x = int("abc")
except ValueError as e:
    print("Ошибка:", e)
print(e)
Ошибка: invalid literal for int() with base 10: 'abc'
Traceback (most recent call last):
  File "C:\projects\shop\t9.py", line 5, in <module>
    print(e)
          ^
NameError: name 'e' is not defined

Если исключение нужно позже, присвойте его другой переменной внутри except.

raise: выбросить и перевыбросить исключение

raise выбрасывает исключение явно — например, когда функция получила недопустимые данные:

def set_discount(percent):
    if not 0 <= percent <= 100:
        raise ValueError(f"Скидка должна быть от 0 до 100, получено {percent}")
    ...

raise без аргументов внутри except перевыбрасывает текущее исключение с исходным traceback. Это частый приём «залогировать и передать дальше»:

import logging

log = logging.getLogger(__name__)


def read_config(path):
    try:
        with open(path, encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        log.error("Нет файла конфигурации %s", path)
        raise

raise from: цепочка исключений

Когда вы перехватываете низкоуровневую ошибку и выбрасываете вместо неё свою, связывайте их через from e — так в traceback сохранится первопричина:

import json


def load_config(text):
    try:
        return json.loads(text)
    except json.JSONDecodeError as e:
        raise ValueError("Некорректный конфиг") from e

Traceback покажет оба исключения, разделённые строкой The above exception was the direct cause of the following exception:. Если исходное исключение действительно не важно для читателя, его можно скрыть через from None:

def get_price(prices, name):
    try:
        return prices[name]
    except KeyError:
        raise ValueError(f"Неизвестный товар: {name}") from None
Traceback (most recent call last):
  File "C:\projects\shop\prices.py", line 8, in <module>
    get_price({"apple": 100}, "banana")
  File "C:\projects\shop\prices.py", line 5, in get_price
    raise ValueError(f"Неизвестный товар: {name}") from None
ValueError: Неизвестный товар: banana

Без from Python тоже покажет оба исключения, но с фразой During handling of the above exception, another exception occurred: — так выглядит случайная ошибка внутри обработчика, а не осознанная замена.

Свои исключения

Собственное исключение — это класс, унаследованный от Exception. От BaseException наследуют только системные исключения вроде KeyboardInterrupt и SystemExit, и своё исключение от него делать не нужно. Для приложения удобно завести базовый класс и наследовать от него конкретные ошибки:

class AppError(Exception):
    """Базовое исключение приложения."""


class NotFoundError(AppError):
    pass


class InsufficientFundsError(AppError):
    def __init__(self, balance, amount):
        super().__init__(f"Баланс {balance}, а списать нужно {amount}")
        self.balance = balance
        self.amount = amount


def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(balance, amount)
    return balance - amount


try:
    withdraw(100, 250)
except InsufficientFundsError as e:
    print(e)                     # → Баланс 100, а списать нужно 250
    print(e.amount - e.balance)  # → 150

Вызывающий код может ловить конкретную ошибку (except NotFoundError) или все ошибки приложения разом (except AppError). Названия классов по соглашению заканчиваются на Error.

contextlib.suppress: игнорировать ожидаемую ошибку

Когда исключение действительно не важно, вместо try/except: pass можно написать короче:

import os
from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("cache.tmp")

Это эквивалент:

try:
    os.remove("cache.tmp")
except FileNotFoundError:
    pass

Используйте suppress только для конкретных и заведомо безопасных исключений.

ExceptionGroup и except* (Python 3.11+)

С Python 3.11 можно выбросить сразу несколько несвязанных исключений, упаковав их в ExceptionGroup, — например, все ошибки валидации формы или ошибки нескольких параллельных задач asyncio.TaskGroup. Для обработки групп появился синтаксис except*: каждый блок обрабатывает свою часть группы.

def validate(user):
    errors = []
    if not user.get("name"):
        errors.append(ValueError("пустое имя"))
    if not isinstance(user.get("age"), int):
        errors.append(TypeError("возраст должен быть числом"))
    if errors:
        raise ExceptionGroup("ошибки валидации", errors)


try:
    validate({"name": "", "age": "20"})
except* ValueError as eg:
    for err in eg.exceptions:
        print("ValueError:", err)
except* TypeError as eg:
    for err in eg.exceptions:
        print("TypeError:", err)
ValueError: пустое имя
TypeError: возраст должен быть числом

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

Чего не делать

Голый except

# Плохо
try:
    process()
except:
    pass

Голый except: перехватывает вообще всё, включая KeyboardInterrupt (Ctrl+C) и SystemExit (sys.exit()), — программу становится трудно даже остановить. Если нужен «перехват всего», пишите except Exception, но и это оправдано только на верхнем уровне: в обработчике веб-запроса, в цикле фоновой задачи, — и обязательно с логированием.

Проглатывание ошибок

# Плохо: ошибка исчезла, данные молча не сохранились
try:
    save_order(order)
except Exception:
    pass

Пустой обработчик превращает понятный traceback в загадочный баг: программа «работает», но делает не то. Минимум — залогировать исключение с traceback:

import logging

log = logging.getLogger(__name__)

try:
    save_order(order)
except Exception:
    log.exception("Не удалось сохранить заказ %s", order.id)
    raise

Слишком широкий try

# Плохо: KeyError может прилететь из любой из строк
try:
    user = users[user_id]
    discount = settings["discount"]
    total = calc_total(user, discount)
except KeyError:
    total = 0

Оборачивайте в try только ту операцию, ошибку которой вы обрабатываете. Остальное — до try или в else.

Исключения вместо простой проверки

Для словарей dict.get(key, default) часто читается лучше, чем try/except KeyError. Но в целом стиль Python допускает оба подхода: «проще попросить прощения, чем разрешения» (EAFP) — нормальная практика, когда проверка заранее сложна или ненадёжна, как с существованием файла. Методы словаря описаны в статье словари в Python.

Частые вопросы

Можно ли использовать try без except?

Да, если есть finally: try/finally гарантирует выполнение кода очистки, но исключение после этого продолжит всплывать.

Как вывести полный traceback в except?

Используйте logging.exception("сообщение") или traceback.print_exc() из модуля traceback. Обе функции печатают traceback текущего исключения и не останавливают программу.

Что лучше: return в finally или нет?

Не пишите return в finally: он отменяет исключение, которое возникло в try, и ошибка тихо пропадает. То же делают break и continue, которые выходят из finally. С Python 3.14 (PEP 765) компилятор выдаёт на такой код SyntaxWarning: 'return' in a 'finally' block; предупреждение появляется при компиляции файла, а не при каждом выполнении.

Как поймать ModuleNotFoundError?

Так же, как любое исключение: except ModuleNotFoundError. Это используют для необязательных зависимостей. Но если импорт обязателен, ошибку нужно исправлять, а не прятать — см. статью ModuleNotFoundError: No module named.

Как повторять ввод, пока пользователь не введёт правильное значение?

Поместите try/except в бесконечный цикл и выходите из него через break после успешного преобразования:

while True:
    try:
        age = int(input("Возраст: "))
        break
    except ValueError:
        print("Нужно целое число, попробуйте ещё раз")

Для повторных попыток сетевых запросов удобнее декоратор — пример retry есть в статье о декораторах.

Замедляет ли try/except программу?

Если исключение не возникло — практически нет: с Python 3.11 вход в блок try ничего не стоит («zero-cost» исключения). Дорого само исключение, поэтому try/except не стоит использовать для ситуации, которая случается на каждой итерации большого цикла, — там быстрее обычная проверка.

Итог

try/except перехватывает конкретные исключения, else выполняется при успехе, finally — всегда. Перехватывайте узкие типы, перевыбрасывайте через raise или raise ... from e, заводите свои классы исключений от Exception и не глотайте ошибки молча. Как читать сами сообщения об ошибках, рассказано в руководстве «Ошибки в Python: как читать traceback»; весь кластер — в разделе «Ошибки и исключения».

  • #Python
  • #try except
  • #Исключения
  • #raise
  • #Обработка ошибок
↑ ↓ выборEnter открытьEsc закрыть