except Exception в Python: когда ловить всё и чем это грозит
Одна строка вместо перечня типов: except Exception ловит почти всё. Разбираем слепую зону перехвата, спрятанные баги и место, где широкая сетка уместна по-настоящему.
Редакция Питоники
Урок 9 об иерархии показал: ловля родителя ловит и детей, а почти вся рабочая семья ошибок живёт под Exception. Отсюда соблазн — вместо перечисления типов написать except Exception и закрыть вопрос разом. Программа действительно перестаёт падать. У этого урока две задачи: показать, где широкая сетка — правильный инструмент, и что происходит с кодом, где она стоит на месте конкретных типов.
Что именно ловит except Exception
Exception — родитель почти всех ошибок, с которыми программа сталкивается в работе: ValueError, KeyError, TypeError, FileNotFoundError, ZeroDivisionError и ваших собственных классов из урока 10. Один except с этим типом — сетка с крупными ячейками: в неё попадает что угодно, лишь бы это был ребёнок Exception. Проверим на трёх разных ошибках сразу:
def make_error(kind):
if kind == 'num':
return int('abc')
if kind == 'key':
return {}['имя']
return 'text' + 5
for kind in ['num', 'key', 'type']:
try:
make_error(kind)
except Exception as e:
print(kind, '->', type(e).__name__, '-', e)
num -> ValueError - invalid literal for int() with base 10: 'abc' key -> KeyError - 'имя' type -> TypeError - can only concatenate str (not "int") to str
Три разные проблемы — один обработчик. Переменная e из урока 5 каждый раз получает свой объект ошибки, а type(e).__name__ показывает, кто именно прилетел: это минимальная гигиена широкого перехвата, без неё вы не узнаете даже тип. Свои исключения-классы ловятся той же сеткой — наследование от Exception сделало их детьми:
class InsufficientFundsError(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(f'не хватает {amount - balance}')
return balance - amount
try:
withdraw(100, 150)
except Exception as e:
print('поймано:', type(e).__name__, '-', e)
поймано: InsufficientFundsError - не хватает 50
Кого Exception не ловит
У широкого перехвата есть слепая зона, и она задумана как защита. Прямо от BaseException, мимо Exception, наследуются SystemExit — сигнал выхода и KeyboardInterrupt — прерывание по Ctrl+C. except Exception их пропускает: выход из программы и просьба пользователя остановиться не считаются ошибками данных. Если бы сетка ловила и их, программу было бы не выключить.
def quit_app():
raise SystemExit('пора выходить')
try:
try:
quit_app()
except Exception as e:
print('Exception поймал:', type(e).__name__)
except BaseException as e:
print('мимо Exception проскочил:', type(e).__name__)
print('программа ещё работает')
мимо Exception проскочил: SystemExit программа ещё работает
Внешний except BaseException понадобился только для демонстрации: он ловит вообще всё, включая SystemExit, — и в обычном коде так не пишут. Из примера важно другое: SystemExit прошёл сквозь внутренний except Exception не пойманным. А голый except: без типа — то же самое, что except BaseException:: самая широкая сетка из возможных.
try:
risky_operation()
except: # это except BaseException - ловит даже Ctrl+C
print('какая-то проблема')
Чем грозит: проглоченный баг
Самая дорогая цена широкого перехвата — спрятанный баг. Представьте отчёт по числам: в функции подсчёта опечатка, и вместо len написано lne. NameError — это не грязные данные, это сломанный код. Но except Exception не различает одно от другого:
def average(values):
total = sum(values)
return total / lne(values) # опечатка: должно быть len
try:
print('среднее:', average([2, 4, 6]))
except Exception:
print('не получилось, идём дальше')
print('отчёт готов')
не получилось, идём дальше отчёт готов
Программа напечатала отчёт готов и ушла дальше, хотя среднее не посчитано вовсе. Уберите try — и traceback из первого урока честно покажет строку с опечаткой:
print('среднее:', average([2, 4, 6]))
Где широкий перехват уместен
Есть место, где ловить всё — не лень, а архитектура. Граница программы: фоновый обработчик очереди задач, главный цикл сервера, обёртка вокруг чужого плагина. Там правило другое: одна упавшая задача не должна ронять соседние. Программа обязана пережить любую ошибку внутри задачи — даже ту, тип которой автор обработчика не предвидел. Границ в настоящей системе больше, чем кажется: ежеминутная задача планировщика, обработчик запроса веб-сервера, разбор входящего сообщения. Во всех случаях упавший элемент — не повод будить дежурного ночью: пусть видит запись в журнале утром. Именно поэтому фреймворки сами ставят широкий перехват вокруг пользовательского кода — печатают traceback и живут дальше вместо того, чтобы падать целиком.
tasks = [
('письмо', 'Анне'),
('число', 'abc'),
('письмо', 'Борису'),
]
log = []
for name, arg in tasks:
try:
if name == 'число':
int(arg)
print('разобрано число:', arg)
else:
print('отправлено:', arg)
except Exception as e:
log.append(f'{name}: {type(e).__name__} - {e}')
print('задачи кончились, журнал:')
for line in log:
print(' ', line)
отправлено: Анне отправлено: Борису задачи кончились, журнал: число: ValueError - invalid literal for int() with base 10: 'abc'
Средняя задача упала, журнал получил запись, соседние выполнены. Это и есть формула уместного широкого перехвата, и в ней три условия сразу: перехват стоит на границе, ошибка фиксируется в журнале, продолжение работы осмысленно. Уберите любой пункт — и сетка превратится из страховки в дыру. Граница — не единственный сценарий: второй легальный приём — залогировать и пробросить дальше, как учил урок 8 о голом raise. Тогда перехват не прячет ошибку, а добавляет ей контекст:
def process(value):
return int(value)
try:
try:
process('7x')
except Exception as e:
print('журнал:', type(e).__name__, '-', e)
raise
except ValueError:
print('ошибка ушла наверх тем же типом')
журнал: ValueError - invalid literal for int() with base 10: '7x' ошибка ушла наверх тем же типом
| Ситуация | Решение |
|---|---|
| Фоновая задача в очереди — упавшая не должна ронять соседние | except Exception с записью в журнал и продолжением |
| Верхний уровень долгоживущего процесса | except Exception, лог, перезапуск цикла |
| Разбор значения с заранее известным риском — не число | except ValueError — конкретный тип |
| Тело функции, где возможна опечатка | Никаких широких перехватов: пусть падает и показывает строку |
| Реакция на Ctrl+C | KeyboardInterrupt не перехватывать вовсе |
Три вопроса перед except Exception
- граница ли это: код принимает задания от чужого кода или считает сам?
- что попадёт в журнал: тип ошибки, сообщение, контекст задачи?
- осмысленно ли продолжение: программа без этой операции всё ещё полезна?
Три ответа «да» — пишите except Exception спокойно. Хотя бы один «нет» — сузьте до конкретного типа из урока 3. Широкая сетка — инструмент границы, а не замена конкретике; следующий урок ставит её на рабочее место: циклы, которые переживают сбои — там except внутри for обрабатывает поток данных построчно.
Сначала предскажи ответ в голове — это главный навык программиста.
try:
print(int('12x'))
except Exception as e:
print(type(e).__name__, '-', e)
print('конец')
try:
try:
raise SystemExit('выход')
except Exception:
print('внутренний поймал')
except BaseException:
print('внешний поймал')
print('работаем')
tasks = ['1', 'x', '3']
good = 0
for t in tasks:
try:
int(t)
good += 1
except Exception:
pass
print('годных:', good)
1. Какие исключения except Exception НЕ перехватывает?
2. Чем голый except: отличается от except Exception?
3. Почему except Exception внутри модуля считается опасным?
4. Где широкий перехват уместен по-настоящему?
5. Что обязательно делать в except Exception на границе программы?
6. Как залогировать ошибку и при этом не спрятать её от вызывающего кода?
7. Поймано своё исключение InsufficientFundsError(Exception). Что напечатает type(e).__name__ в except Exception?
В списке tasks три задачи: два письма и одно число со значением 12x. Обработайте все циклом: письма печатайте как отправленные (отправлено: адрес), число разберите int-ом. Ошибку любой задачи ловите except Exception, добавляйте в log строку вида тип - сообщение, а после цикла напечатайте журнал построчно.
Что ловит except Exception в Python?
Все исключения, унаследованные от Exception: ValueError, KeyError, TypeError, FileNotFoundError, ZeroDivisionError и пользовательские классы. Не ловятся SystemExit и KeyboardInterrupt — они наследуются прямо от BaseException, чтобы выход из программы и Ctrl+C всегда работали.
Урок 9: иерархия исключенийЧем except Exception отличается от голого except?
Голый except равен except BaseException: он шире и глотает даже KeyboardInterrupt, из-за чего программу нельзя остановить по Ctrl+C. except Exception — самая широкая сетка, которую допустимо писать: она оставляет системные сигналы в покое.
Почему except Exception считается плохой практикой?
Потому что он прячет баги: опечатка в имени переменной даёт NameError — и широкая сетка ловит её так же, как грязные данные. Программа продолжает работу без результата и без указания на сломанную строку. Конкретные типы внутри модуля оставляют багам честный traceback.
Урок 3: except ValueErrorКак ловить все ошибки и не прятать баги?
Широкий перехват ставить только на границе программы, фиксировать каждую ошибку в журнале — хотя бы тип и сообщение через type(e).__name__ и e — и при необходимости пробрасывать голым raise. Тогда ни одна ошибка не исчезает молча, а вызывающий код видит исходный тип.
Урок 8: голый raiseПоймает ли except Exception моё собственное исключение?
Да, если класс унаследован от Exception — а это стандарт для своих ошибок: class InsufficientFundsError(Exception). Ловля родителя ловит детей, поэтому собственный класс попадает и в except Exception, и в перечисление через несколько except-блоков.
Урок 10: свои исключенияПонравился урок? Сошлитесь на него
«NameError, AttributeError и TypeError — сигналы сломанного кода, а не грязных данных.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Исключения · Урок 9
Иерархия исключений Python: кто чей родитель
ZeroDivisionError, KeyError, FileNotFoundError — не набор случайных имён, а семейное дерево. Разбираемся, кто чей родитель и почему ловля родителя ловит детей.
Исключения · Урок 8
Повторное возбуждение: голый raise в except
Исключение поймано, но проблема не решена: голый raise внутри except передаёт её наверх, не теряя стек. Учимся честному пробросу и ловим RuntimeError от raise вне except.
Исключения · Урок 16
Ошибки в цикле Python: try внутри for и продолжение после сбоя
Сто строк данных, одна битая — и без защиты падает весь проход. Ставим try вокруг одного элемента: сбой обрабатывается, цикл идёт дальше, сбои считаются и попадают в журнал.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
aiogram · Урок 9
Обработка ошибок в телеграм-боте: try/except, лимиты и логи
Учим бот переживать ошибки: try/except в хендлерах, лимиты Telegram с RetryAfter, модуль logging вместо print и перезапуск polling после падения.
обработка ошибок pythonошибки телеграм бота
Flask · Урок 11
Обработка ошибок во Flask: свои страницы 404 и 500
Одиннадцатый урок курса Flask: гость опечатался в адресе — и видит английскую заглушку без вашего меню. Учимся отдавать свои страницы 404 и 500 через @app.errorhandler, честно отвечать abort(404), когда записи нет, и проверять статусы через test_client.
flask обработка ошибок 404flask страница ошибки 500
Исключения · Урок 3
except ValueError: ловим конкретный тип ошибки
Уточняем обработчик: except ValueError ловит только свой тип ошибок. Почему голый except прячет опечатки и как ValueError ведёт себя у int и list.remove.
except valueerror pythontry except python