Исключение (exception) в Python — это объект, который интерпретатор создает и «поднимает», когда во время выполнения нарушается нормальный ход программы: деление на ноль, обращение к несуществующему ключу, чтение отсутствующего файла. Если исключение не перехватить, программа аварийно завершится с трассировкой стека. Ниже разбираю механизм try/except/else/finally, оператор raise, иерархию классов, свои исключения, менеджеры контекста и группы исключений в Python 3.14.
Содержание
- Ошибка и исключение — не одно и то же
- Минимальный рабочий пример: try/except/else/finally
- Перехват конкретного типа против голого except
- raise: как поднять исключение самому
- Как читать traceback
- Иерархия встроенных исключений
- Свои классы исключений
- Менеджеры контекста: with вместо ручного finally
- Группы исключений и except* (Python 3.11+)
- Выводы
- Где применяется / связь с практикой
- FAQ
Ошибка и исключение — не одно и то же
Термины часто путают, разведу их сразу.
- Синтаксическая ошибка (SyntaxError) ловится еще до запуска, на этапе разбора кода. Программа не стартует вообще.
- Исключение времени выполнения возникает, когда код синтаксически корректен, но при работе наступает «особое» условие. Именно такие ситуации и обрабатывают через try/except.
- Логическая ошибка — код работает и не падает, но выдает неверный результат. Ее исключение не поймает, ее ищут тестами.
Синтаксическую ошибку исправляют в редакторе, исключение — обрабатывают в рантайме. Дальше речь только про них.
Минимальный рабочий пример: try/except/else/finally
Начну с законченного примера, который можно скопировать и запустить. Он показывает все четыре блока сразу.
def read_age(raw):
try:
age = int(raw) # опасная операция
except ValueError:
print("нужно целое число")
return None
else:
print(f"возраст принят: {age}") # только если try прошел без ошибок
return age
finally:
print("проверка завершена") # выполняется всегда
read_age("30")
read_age("тридцать")
Результат:
возраст принят: 30
проверка завершена
нужно целое число
проверка завершена
Разберу по блокам:
- try содержит опасный код, который может «поднять» исключение (здесь
int(raw)). - except ValueError срабатывает, только если внутри try возникло именно это исключение. Для строки «тридцать»
intподнимает ValueError. - else выполняется, если try прошел без исключений. Сюда выносят продолжение, которое не должно попадать под except.
- finally выполняется всегда: и при успехе, и при ошибке, и даже при
return. Отсюда закрывают ресурсы.
Обратите внимание на порядок: для «30» строка из else печатается раньше строки из finally — finally отрабатывает перед фактическим возвратом значения.
Перехват конкретного типа против голого except
Частая ошибка новичка — написать except: без типа. Такой «голый» except ловит вообще все, включая опечатки в коде и служебные сигналы вроде остановки с клавиатуры. Баг маскируется под штатную обработку.
Неверно (голый except прячет опечатку в ключе):
data = {"count": "5"}
try:
value = int(data["cout"]) # опечатка: cout вместо count
except:
value = 0
print(value)
Результат:
0
Программа напечатала 0 и не подала виду: на самом деле сработал KeyError из-за опечатки, а не проблема с числом. Логика молча сломалась. Исправление — перехватывать конкретные типы:
data = {"count": "5"}
try:
value = int(data["count"])
except (KeyError, ValueError):
value = 0
print(value)
Результат:
5
Правило простое: перехватывайте самый узкий тип, который реально ожидаете. Несколько типов — кортежем в скобках или отдельными блоками except (частные случаи раньше, общие позже). except Exception допустим как осознанный «последний рубеж» с логированием, голый except: без типа — антипаттерн.
raise: как поднять исключение самому
Оператор raise возбуждает исключение вручную — когда данные формально корректны для интерпретатора, но нарушают правила вашей задачи.
def withdraw(balance, amount):
if amount > balance:
raise ValueError(f"нельзя снять {amount} при балансе {balance}")
return balance - amount
print(withdraw(100, 30)) # 70
print(withdraw(100, 500)) # ValueError: нельзя снять 500 при балансе 100
Результат:
70
Первый вызов вернул 70 и продолжил работу, второй поднял ValueError и оборвал программу с трассировкой (как ее читать — в следующем разделе). Внутри except исключение можно перевозбудить через raise без аргументов (сохранит трассировку) или обернуть в другое через raise NewError(...) from err — тогда будет видна цепочка причин.
Как читать traceback
Traceback — это отчет о том, где и почему упала программа. Читают его снизу вверх: последняя строка называет тип и текст исключения, а выше идет путь вызовов — от места запуска до конкретной строки, где произошел сбой.
print(10 / 0)
Результат:
Traceback (most recent call last):
File "main.py", line 1, in <module>
print(10 / 0)
~~~^~~
ZeroDivisionError: division by zero
Начиная с Python 3.11 трассировка подчеркивает точное выражение-виновник (стрелки ~~~^~~), поэтому даже в длинной строке видно, какая именно операция упала. Тип здесь — ZeroDivisionError, текст — division by zero.
Иерархия встроенных исключений
Все исключения — это классы в дереве наследования. На вершине стоит BaseException; от него отходят системные сигналы и обычные ошибки.
| Класс | Роль | Ловить через except Exception? |
|---|---|---|
| BaseException | Корень всей иерархии | Нет, слишком широко |
| SystemExit | Выход через sys.exit() |
Нет |
| KeyboardInterrupt | Прерывание с клавиатуры (Ctrl+C) | Нет |
| Exception | База для прикладных ошибок | Да |
| ValueError, KeyError, TypeError | Частые ошибки данных | Да, наследники Exception |
Ключевой момент: except Exception перехватывает прикладные ошибки, но НЕ трогает SystemExit и KeyboardInterrupt — они наследуются напрямую от BaseException, в обход Exception. Поэтому Ctrl+C прерывает программу, даже если внутри стоит except Exception: пользователь всегда должен иметь возможность остановить процесс. Родство можно проверить в коде — issubclass(KeyError, Exception) вернет True, а issubclass(KeyboardInterrupt, Exception) вернет False.
Свои классы исключений
Для доменных ошибок заводят свой класс — наследника Exception. Так вызывающий код сможет перехватывать именно вашу ошибку, не путая с чужими.
class NegativeAgeError(Exception):
"""возраст не может быть отрицательным"""
def set_age(value):
if value < 0:
raise NegativeAgeError(f"получено {value}")
return value
try:
set_age(-5)
except NegativeAgeError as e:
print(f"ошибка домена: {e}")
Результат:
ошибка домена: получено -5
Практика такая: наследуйтесь от Exception (не от BaseException), давайте классу говорящее имя с суффиксом Error, а текст причины передавайте в конструктор. Для крупного проекта заводят один базовый класс модуля (например AppError) и от него частные, чтобы вызывающий мог поймать все ошибки приложения одним except.
Менеджеры контекста: with вместо ручного finally
Закрывать ресурсы через finally надежно, но многословно. Менеджер контекста и оператор with делают это автоматически: ресурс освобождается при выходе из блока — и при нормальном завершении, и при исключении внутри.
with open("data.txt", "w", encoding="utf-8") as f:
f.write("привет")
# здесь файл уже закрыт, даже если бы write поднял исключение
Конструкция with эквивалентна try/finally с закрытием файла в finally, но короче и надежнее — невозможно забыть освободить ресурс. Тот же механизм используют блокировки, соединения с БД, сетевые сессии. Свой менеджер контекста делают через класс с методами __enter__ и __exit__.
Группы исключений и except* (Python 3.11+)
Иногда операция порождает сразу несколько независимых ошибок — например при параллельной обработке нескольких задач. Для этого с Python 3.11 появились ExceptionGroup и синтаксис except* (со звездочкой), актуальные и в Python 3.14.
def process():
raise ExceptionGroup(
"две проблемы",
[ValueError("плохое значение"), KeyError("нет ключа")],
)
try:
process()
except* ValueError as eg:
print(f"поймал ValueError: {eg.exceptions}")
except* KeyError as eg:
print(f"поймал KeyError: {eg.exceptions}")
Результат:
поймал ValueError: (ValueError('плохое значение'),)
поймал KeyError: (KeyError('нет ключа'),)
Различие терминов важно: обычный except берет первое подходящее одиночное исключение, а except* работает с группой и может выполнить несколько веток — по одной на каждый встреченный тип. Обычный код по-прежнему пишут через except; except* нужен там, где ошибки приходят пачкой (типичный сценарий — asyncio.TaskGroup).
Выводы
- Исключение — объект-сигнал о нарушении хода программы во время выполнения; синтаксическая и логическая ошибки — другие сущности, их try/except не покрывает.
- Механизм: try (опасный код), except (конкретный тип), else (продолжение без ошибок), finally (всегда). Голый
except:без типа — антипаттерн, прячет баги. - raise поднимает исключение вручную; traceback читают снизу вверх, тип и причина — в последней строке.
- Иерархия начинается с BaseException;
except Exceptionловит прикладные ошибки, но не SystemExit и KeyboardInterrupt. - Свои ошибки наследуют от Exception; ресурсы освобождают через with; пачку ошибок обрабатывают через ExceptionGroup и except* (с 3.11, актуально в 3.14).
Где применяется / связь с практикой
Обработка исключений — обязательный навык для любого backend- и скриптового кода на Python: валидация ввода, работа с файлами и сетью, обращения к БД, парсинг данных. Умение перехватывать нужный тип, поднимать свои ошибки и освобождать ресурсы отличает надежный код от того, что падает на первом нестандартном вводе.
Освойте тему на практике
Системно эти темы разбирают на курсе Python Basic: от синтаксиса до работы с ошибками и стандартной библиотекой. Посмотреть формат занятий можно на открытых уроках Otus — они бесплатные и проходят вживую.
FAQ
Чем except Exception отличается от голого except?
except Exception ловит прикладные ошибки, но пропускает SystemExit и KeyboardInterrupt, так что Ctrl+C по-прежнему останавливает программу. Голый except: перехватывает и их тоже, из-за чего процесс трудно прервать, а опечатки маскируются под штатную обработку.
Нужен ли else, если можно все написать в try?
else помогает не прятать под except лишний код: в try оставляют только опасную операцию, а продолжение выносят в else. Это снижает риск случайно поймать чужое исключение.
Когда заводить свой класс исключения, а не брать ValueError?
Когда вызывающий код должен отличать вашу доменную ошибку от стандартных и реагировать на нее отдельно. Для простой проверки значения достаточно встроенного ValueError или TypeError.



