Исключения в Python: try, except, else, finally, raise и свои исключения
Исключение в 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: от новичка до профи».
Перейти к курсу