Основы

Исключения в Python: try, except, else, finally, raise и свои исключения

Обновлено: · Skillaria.Top

Исключение в Python — объект ошибки, который прерывает выполнение кода, пока его не перехватит блок try/except; else выполняется, если ошибки не было, а finally — всегда. Вызвать исключение самому можно инструкцией raise, например raise ValueError("сумма отрицательная"), а своё исключение создаётся классом-наследником Exception.

Исключение в Python — это объект, который сигнализирует об ошибке и прерывает обычный ход программы. Если его никто не перехватил, Python печатает traceback и завершает работу. Перехватывают исключения конструкцией try/except, а вызывают сами — инструкцией raise. Разберём, как это устроено, какие ошибки встречаются чаще всего и как не превратить обработку ошибок в их сокрытие.

Что такое исключение в Python

Когда операция не может выполниться — деление на ноль, обращение к несуществующему ключу, превращение слова в число, — Python создаёт объект исключения и «выбрасывает» его. Выполнение текущей функции останавливается, исключение поднимается по цепочке вызовов, пока не встретит подходящий обработчик. Не встретило — программа падает.

>>> 10 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

Кстати, ответ на частый вопрос: при делении на ноль в Python возникает исключение ZeroDivisionError. Это касается и /, и //, и %.

Как читать traceback

Traceback (трейсбек) — отчёт о том, как программа дошла до ошибки. Читать его удобнее снизу вверх. Последняя строка — главное: тип исключения и сообщение. Выше — строка кода, где всё случилось, ещё выше — кто её вызвал, и так до точки входа.

Traceback (most recent call last):
  File "tracker.py", line 9, in <module>
    total = parse_amount(row)
  File "tracker.py", line 4, in parse_amount
    return float(raw.strip())
           ~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: 'сто'

С Python 3.11 под строкой кода рисуются значки ~~~^^^ (PEP 657): они показывают, какое именно выражение в строке сломалось. Это особенно помогает, когда в одной строке несколько вызовов подряд.

Подсказки «Did you mean»

С Python 3.10 при опечатке в имени ошибка предлагает близкий вариант. В Python 3.15 подсказки стали умнее: интерпретатор знает привычки из других языков. Вызов list.push подскажет append, "кофе".toUpperCase() — метод upper. Подсказка заглядывает и во вложенные поля: если словарь лежит в атрибуте totals объекта, а вы вызвали items() у самого объекта, ошибка предложит .totals.items. Близкие имена теперь подсказывает и delattr().

Типы исключений в Python

Встроенных исключений десятки, но на старте вы чаще всего увидите эти:

ИсключениеКогда возникаетПример
NameErrorИмя не определено, чаще всего опечаткаprint(totl)
TypeErrorОперация не подходит для этого типа"5" + 3
ValueErrorТип верный, значение нетint("сто")
KeyErrorНет такого ключа в словареd["missing"]
IndexErrorИндекс за пределами списка[1, 2][5]
AttributeErrorУ объекта нет метода или поля[].push(1)
ZeroDivisionErrorДеление на ноль1 / 0
FileNotFoundErrorФайл не найденopen("nope.txt")

Отдельно стоит SyntaxError (и его частный случай IndentationError): он возникает ещё при разборе файла, до запуска кода, поэтому перехватить его в том же файле через try нельзя.

Иерархия исключений

Исключения — это классы, и они выстроены в иерархию. На вершине — BaseException. От него наследуются Exception (почти все «обычные» ошибки) и несколько системных: KeyboardInterrupt, SystemExit, GeneratorExit.

BaseException
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      └── ValueError

Иерархия важна при перехвате: except LookupError поймает и KeyError, и IndexError. А KeyboardInterrupt — это не ошибка в коде: так Python сообщает, что пользователь нажал Ctrl+C. Он не наследуется от Exception специально, чтобы except Exception не мешал прервать программу.

Обработка исключений: try/except/else/finally

Код, который может упасть, кладут в блок try. В блоке except указывают тип ошибки и что с ней делать. Именно except — это блок, который в Python используется для обработки исключений.

def parse_amount(raw):
    try:
        value = float(raw)
    except ValueError as e:
        print(f"Пропускаю строку: {e}")
        return None
    else:
        return value          # выполняется, если ошибки не было
    finally:
        print("разбор завершён")   # выполняется всегда

print(parse_amount("250"))

Порядок вывода здесь — сначала «разбор завершён», потом 250.0. return в блоке else запоминает значение, затем Python обязательно выполняет finally и только после этого возвращает результат. Поэтому finally надёжен для уборки: закрыть файл, отпустить соединение.

Если в finally написать свой return, он перекроет предыдущий и даже «проглотит» исключение. С Python 3.14 за такой код выдаётся SyntaxWarning (PEP 765) — не пишите return в finally.

Несколько исключений сразу

Можно написать несколько блоков except под разные типы или перечислить типы в кортеже:

try:
    amount = int(data["amount"])
except KeyError:
    amount = 0
except (ValueError, TypeError) as e:
    print("Кривая сумма:", e)
    amount = 0

Блоки проверяются сверху вниз, срабатывает первый подходящий, поэтому более конкретные типы ставят выше общих. С Python 3.14 (PEP 758) несколько типов можно перечислить и без скобок — except ValueError, TypeError:, — но если есть as, скобки по-прежнему обязательны.

Как вывести текст ошибки и узнать её тип

Объект исключения доступен через as e. str(e) даёт сообщение, type(e).__name__ — имя типа:

try:
    int("сто")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(e)                  # invalid literal for int() with base 10: 'сто'

Модуль traceback: полный текст ошибки

Иногда ошибку нужно перехватить, но сохранить полный трейсбек — для лога или отчёта. Для этого есть модуль traceback:

import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()             # печатает трейсбек в stderr
    text = traceback.format_exc()     # тот же текст строкой

Если вы пишете логи через модуль logging, проще вызвать logging.exception("не удалось посчитать") внутри except — трейсбек добавится к сообщению автоматически.

Python raise: как вызвать исключение

Инструкция raise выбрасывает исключение вручную. Так функция сообщает, что не может выполнить работу с такими данными:

def add_expense(amount):
    if amount < 0:
        raise ValueError(f"Сумма не может быть отрицательной: {amount}")
    ...

add_expense(-5)
# ValueError: Сумма не может быть отрицательной: -5

Python raise — это не функция, а инструкция, поэтому скобки после слова raise не нужны: в скобках передаются аргументы конструктору исключения. Выбирайте тип по смыслу: неверное значение — ValueError, неверный тип — TypeError.

raise без аргументов и raise from

Внутри except голый raise пробрасывает текущее исключение дальше без изменений. Это нужно, когда вы хотите что-то записать в лог, но обработку оставить вызывающему коду.

Если вы заменяете одно исключение другим, используйте raise ... from e — исходная ошибка сохранится в цепочке, и в трейсбеке будет видно обе:

def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"Нет конфига {path}") from e

add_note: дописать контекст к ошибке

С Python 3.11 (PEP 678) к исключению можно добавить заметку методом add_note() и выбросить его дальше. Тип ошибки не меняется, а заметка печатается под сообщением:

def parse(raw, line_no):
    try:
        return float(raw)
    except ValueError as e:
        e.add_note(f"файл expenses.csv, строка {line_no}")
        raise

parse("сто", 17)
# ValueError: could not convert string to float: 'сто'
# файл expenses.csv, строка 17

Как создать своё исключение в Python

Свои исключения нужны для ошибок предметной области: превышен бюджет, заказ уже оплачен, пользователь заблокирован. Достаточно класса-наследника Exception:

class BudgetExceeded(Exception):
    def __init__(self, category, limit, spent):
        super().__init__(f"{category}: лимит {limit}, потрачено {spent}")
        self.category = category
        self.limit = limit
        self.spent = spent

def check_budget(category, limit, spent):
    if spent > limit:
        raise BudgetExceeded(category, limit, spent)

try:
    check_budget("Еда", 10000, 12500)
except BudgetExceeded as e:
    print("Предупреждение:", e)   # Предупреждение: Еда: лимит 10000, потрачено 12500

Теперь вызывающий код ловит именно превышение бюджета и не путает его с опечатками. В большом проекте удобно завести общий базовый класс (class TrackerError(Exception)) и наследовать от него все свои исключения — тогда их можно перехватить одним except TrackerError. Наследуйтесь от Exception, а не от BaseException.

Группы исключений и except*

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

def check_all(rows):
    errors = []
    for r in rows:
        try:
            float(r)
        except ValueError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("ошибки в файле", errors)

try:
    check_all(["1", "a", "b"])
except* ValueError as eg:
    print(f"Кривых строк: {len(eg.exceptions)}")   # Кривых строк: 2

Python try except все ошибки: почему так делать нельзя

Самая вредная привычка — ловить всё подряд: except: pass. Программа «перестаёт падать», но на деле перестаёт сообщать о проблемах. Голый except ловит даже KeyboardInterrupt и SystemExit, так что программу не остановить по Ctrl+C. except Exception: pass чуть лучше, но тоже молча проглатывает опечатки в именах и ошибки логики.

Как корректно обрабатывать исключения в Python:

  • ловите только тот тип, который умеете обработать: except ValueError, а не всё;
  • держите блок try коротким — только строки, которые могут упасть именно так;
  • если ловите широко (except Exception) на верхнем уровне программы, обязательно логируйте трейсбек;
  • не используйте исключения для обычного ветвления там, где хватает проверки: dict.get() вместо try/except KeyError в простых случаях;
  • для ресурсов предпочитайте with — он сам закроет файл даже при ошибке.

Итог

Исключения в Python — нормальный механизм сообщать об ошибках, а не повод их прятать. Читайте traceback снизу вверх, перехватывайте конкретные типы в try/except, используйте else для кода «если всё прошло» и finally для уборки. Python raise позволяет сообщить об ошибке самому, raise from и add_note сохраняют контекст, а свои исключения наследуются от Exception. В курсе по Python эта тема разобрана на практике: трекер расходов перестаёт падать из-за одной кривой строки CSV и выражает превышение бюджета своим исключением.

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

Чем ошибка отличается от исключения в Python?

В разговорной речи это синонимы. Технически SyntaxError находится ещё при разборе кода, а остальные ошибки возникают во время выполнения в виде объектов-исключений, которые можно перехватить.

Можно ли продолжить выполнение после исключения с того же места?

Нет. После перехвата выполнение продолжается после блока try/except, а не со строки, где возникла ошибка. Если нужно обработать каждую строку файла, кладите try внутрь цикла.

Что будет, если исключение возникло внутри except?

Оно поднимется дальше, а исходное исключение сохранится в его атрибуте __context__. В трейсбеке появится фраза During handling of the above exception, another exception occurred.

Как проверить в тестах, что функция выбрасывает исключение?

В pytest используют конструкцию with pytest.raises(ValueError):, в unittest — self.assertRaises(ValueError). Тест пройдёт, только если внутри блока возникло исключение нужного типа.

Что такое assert и чем он отличается от raise?

assert условие, "сообщение" выбрасывает AssertionError, если условие ложно. Это инструмент для проверок при разработке: при запуске с флагом -O assert отключаются, поэтому проверять ввод пользователя нужно через if и raise.

Разберите тему на видео

Эта тема подробно разобрана с практикой в курсе «Python 3.15: от новичка до профи».

Перейти к курсу