Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 15 из 20 Начальный 15 мин 100 XP

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 их пропускает: выход из программы и просьба пользователя остановиться не считаются ошибками данных. Если бы сетка ловила и их, программу было бы не выключить.

SystemExit проскакивает сквозь 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:: самая широкая сетка из возможных.

голый except: так писать не надо
try:
    risky_operation()
except:  # это except BaseException - ловит даже Ctrl+C
    print('какая-то проблема')
Голый except поймает и KeyboardInterrupt: пользователь жмёт Ctrl+C, а программа вместо завершения печатает какая-то проблема и продолжает жить. Минимум вежливости — писать except Exception, чтобы выход и прерывание оставались честными.

Чем грозит: проглоченный баг

Самая дорогая цена широкого перехвата — спрятанный баг. Представьте отчёт по числам: в функции подсчёта опечатка, и вместо 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]))
Честное падение: NameError: name 'lne' is not defined и traceback с точным указанием строки. except Exception превратил этот сигнал в тихое не получилось — отладка стала угадайкой.

Где широкий перехват уместен

Есть место, где ловить всё — не лень, а архитектура. Граница программы: фоновый обработчик очереди задач, главный цикл сервера, обёртка вокруг чужого плагина. Там правило другое: одна упавшая задача не должна ронять соседние. Программа обязана пережить любую ошибку внутри задачи — даже ту, тип которой автор обработчика не предвидел. Границ в настоящей системе больше, чем кажется: ежеминутная задача планировщика, обработчик запроса веб-сервера, разбор входящего сообщения. Во всех случаях упавший элемент — не повод будить дежурного ночью: пусть видит запись в журнале утром. Именно поэтому фреймворки сами ставят широкий перехват вокруг пользовательского кода — печатают 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+CKeyboardInterrupt не перехватывать вовсе

Три вопроса перед 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)
Проверь себя
0 / 7

1. Какие исключения except Exception НЕ перехватывает?

2. Чем голый except: отличается от except Exception?

3. Почему except Exception внутри модуля считается опасным?

4. Где широкий перехват уместен по-настоящему?

5. Что обязательно делать в except Exception на границе программы?

6. Как залогировать ошибку и при этом не спрятать её от вызывающего кода?

7. Поймано своё исключение InsufficientFundsError(Exception). Что напечатает type(e).__name__ в except Exception?

Карточки терминов
Запомнено: 0 / 7
Практика

В списке tasks три задачи: два письма и одно число со значением 12x. Обработайте все циклом: письма печатайте как отправленные (отправлено: адрес), число разберите int-ом. Ошибку любой задачи ловите except Exception, добавляйте в log строку вида тип - сообщение, а после цикла напечатайте журнал построчно.

practice.py
Вопросы и ответы по уроку

Что ловит 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.