try/except в Python: обработка исключений

Python Автор: Среда и версия: CPython 3.14.5
содержание

Конструкция try/except перехватывает исключение времени выполнения, чтобы программа обработала сбой и продолжила работу. В try помещают операцию, которая может завершиться ошибкой; в except указывают конкретный тип исключения и реакцию: показать сообщение, вернуть запасное значение или повторить ввод. Примеры проверены на CPython 3.14.5.

Python / 01

finally объединяет обе ветки

finally объединяет обе ветки01 / вход 02 / операция 03 / результат int("10") успех → else number = 10 int("нет") ValueError → except обработать ошибку после обеих веток finally освободить ресурс01 / вход02 / операция03 / результатint("10")успех → elsenumber = 10int("нет")ValueError → exceptобработать ошибкупосле обеих ветокfinallyосвободить ресурс
После успешного try работает else, после ValueError — подходящий except. Обе ветки затем проходят через finally.

Как работает try/except

Без перехвата первая же ошибка обрывает программу, и следующие строки не выполняются:

age = int("двадцать")
print("сюда не дойдёт")
Traceback (most recent call last):
  File "main.py", line 1, in <module>
    age = int("двадцать")
          ~~~^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'двадцать'

Трассировка читается снизу вверх: последняя строка называет тип ошибки и причину, выше стоит место, где это случилось. Тот же код внутри try не роняет программу:

try:
    age = int("двадцать")
except ValueError as exc:
    print(f"ValueError: {exc}")
ValueError: invalid literal for int() with base 10: 'двадцать'

Часть as exc кладёт сам объект исключения в переменную, и из него достаётся текст ошибки. Без as перехват тоже работает, просто сообщение недоступно.

Блоков except может быть несколько, каждый на свой тип:

data = {"хлеб": 60}
for key in ["хлеб", "масло"]:
    try:
        print(key, data[key])
    except KeyError as exc:
        print(f"нет ключа {exc}")
хлеб 60
нет ключа 'масло'

Обрати внимание: KeyError печатает ключ в кавычках, потому что показывает его представление, а не строку. Способы обойтись без перехвата вовсе, через get и in, разобраны в статье про словари.

Один блок ловит несколько типов, если перечислить их кортежем:

for value in ["10", "десять", None]:
    try:
        print(int(value))
    except (ValueError, TypeError) as exc:
        print(f"{type(exc).__name__}: {exc}")
10
ValueError: invalid literal for int() with base 10: 'десять'
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'

Строка «десять» даёт ValueError, а None даёт TypeError: тип не тот, до разбора содержимого дело не доходит.

Как поймать конкретную ошибку, а не всё подряд

Исключения выстроены в иерархию, и except ловит указанный класс вместе со всеми его потомками. Отсюда правило: узкие типы пишут выше широких, иначе широкий заберёт всё себе.

try:
    int("нет")
except Exception:
    print("поймал Exception")
except ValueError:
    print("поймал ValueError")
поймал Exception

Второй блок недостижим: ValueError — потомок Exception, и до него очередь не доходит. Правильный порядок даёт другой ответ:

try:
    int("нет")
except ValueError:
    print("поймал ValueError")
except Exception:
    print("поймал Exception")
поймал ValueError

Отдельная беда — голый except: без типа. Он ловит вообще всё, включая то, что ловить не надо:

import sys

try:
    sys.exit(1)
except Exception:
    print("Exception не поймал SystemExit")
except BaseException as exc:
    print(f"BaseException поймал {type(exc).__name__}")
BaseException поймал SystemExit

SystemExit и KeyboardInterrupt наследуются от BaseException в обход Exception именно затем, чтобы обычный перехват их пропускал. Голый except: равен except BaseException: и ломает выход из программы вместе с прерыванием по Ctrl+C. Пиши except Exception:, если действительно нужен широкий перехват.

Ещё хуже перехват с пустым телом:

result = None
try:
    result = 1 / 0
except Exception:
    pass
print("result =", result)
result = None

Ошибка проглочена, следов не осталось, переменная тихо осталась пустой и уедет дальше по коду. Если ловишь ошибку, делай с ней что-то: логируй, подставляй запасное значение, поднимай своё исключение.

else и finally: что и когда выполняется

else выполняется, если блок try завершился без исключения и без return, break или continue. finally выполняется при выходе из конструкции независимо от выбранной ветки.

for value in ["10", "нет"]:
    try:
        number = int(value)
    except ValueError:
        print("не число")
    else:
        print("разобрал", number)
    finally:
        print("finally отработал")
разобрал 10
finally отработал
не число
finally отработал

Порядок зависит от исхода: после try выполняется подходящий except при исключении либо else при нормальном завершении; последним выполняется finally.

try:
    print("1 try")
except ValueError:
    print("except")
else:
    print("2 else")
finally:
    print("3 finally")
1 try
2 else
3 finally

Зачем нужен else, если код можно оставить в try. Затем, что try должен содержать только то, что мы готовы перехватить. Положишь в него ещё и обработку результата, и случайный ValueError из этой обработки будет пойман тем же except, который писался про разбор числа. Ошибка окажется замаскирована под чужую.

finally нужен для уборки: закрыть файл, вернуть соединение в пул, снять блокировку. Он отрабатывает и тогда, когда исключение уходит наружу непойманным:

def raises():
    try:
        raise ValueError("сломалось")
    finally:
        print("finally всё равно выполнился")

try:
    raises()
except ValueError as exc:
    print(f"наружу вышло: {exc}")
finally всё равно выполнился
наружу вышло: сломалось

На практике для уборки чаще берут не finally, а with: он делает то же самое короче и не даёт забыть закрытие. Как устроен этот протокол, разобрано в статье про контекстные менеджеры.

Почему return в finally побеждает

Самый частый вопрос про finally на собеседовании. Ответ: возврат из finally перекрывает возврат из try.

def with_finally_return():
    try:
        return "из try"
    finally:
        return "из finally"

print(with_finally_return())
main.py:5: SyntaxWarning: 'return' in a 'finally' block
  return "из finally"
из finally

Функция вернула «из finally», хотя return в try уже отработал. Заодно Python 3.14 сам предупреждает об этой конструкции: SyntaxWarning появляется при компиляции, ещё до вызова функции. То же предупреждение выдаётся на break и continue, если они покидают блок finally, а не только вложенный в него цикл.

Опасность не в самом возврате, а в том, что finally точно так же перекрывает и исключение:

def swallow():
    try:
        raise ValueError("важная ошибка")
    finally:
        return "как будто всё хорошо"

print(swallow())
main.py:5: SyntaxWarning: 'return' in a 'finally' block
  return "как будто всё хорошо"
как будто всё хорошо

Исключение с текстом «важная ошибка» исчезло бесследно: функция вернула строку, вызывающий код ничего не заподозрил. Кроме предупреждения при компиляции, никаких следов не осталось. Не пиши return, break и continue внутри finally.

Без возврата всё ведёт себя предсказуемо: finally отрабатывает и уступает дорогу:

def finally_without_return():
    try:
        return "из try"
    finally:
        print("finally отработал до возврата")

print(finally_without_return())
finally отработал до возврата
из try

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

Оператор raise бросает исключение вручную. Свой класс наследуют от Exception:

class NotEnoughFunds(Exception):
    pass

def withdraw(balance, amount):
    if amount > balance:
        raise NotEnoughFunds(f"не хватает {amount - balance}")
    return balance - amount

try:
    withdraw(1000, 1500)
except NotEnoughFunds as exc:
    print(f"{type(exc).__name__}: {exc}")
NotEnoughFunds: не хватает 500

Свой тип лучше готового ValueError тем, что вызывающий код может поймать именно эту ситуацию, не задевая остальные. Тело класса при этом обычно пустое: всё нужное уже есть в Exception.

Когда одно исключение возникает из-за другого, их связывают через raise ... from:

try:
    try:
        int("нет")
    except ValueError as exc:
        raise RuntimeError("не удалось разобрать конфиг") from exc
except RuntimeError as exc:
    print(f"{exc}")
    print(f"причина: {type(exc.__cause__).__name__}: {exc.__cause__}")
не удалось разобрать конфиг
причина: ValueError: invalid literal for int() with base 10: 'нет'

В трассировке обе ошибки окажутся рядом с пометкой «The above exception was the direct cause of the following exception», а без from связь тоже сохранится, только пометка будет другой, «During handling of the above exception, another exception occurred».

Иногда исключение надо залогировать и пропустить дальше. Для этого raise пишут без аргументов, и он поднимает текущее исключение заново:

try:
    try:
        int("нет")
    except ValueError:
        print("залогировал")
        raise
except ValueError as exc:
    print(f"дошло наружу: {exc}")
залогировал
дошло наружу: invalid literal for int() with base 10: 'нет'

Что try/except поймать не может

Синтаксические ошибки и ошибки отступов. Они возникают при разборе файла, то есть до того, как выполнится хоть одна строка, включая сам try:

try:
    if True
        print("привет")
except SyntaxError:
    print("поймал")
  File "main.py", line 2
    if True
           ^
SyntaxError: expected ':'

Блок except SyntaxError в этом файле не сработает никогда, потому что файл целиком не дошёл до запуска. Разбор таких сообщений собран в статьях про SyntaxError и ошибку отступа.

Исключение из правила — код, который компилируется во время работы программы. Разбор чужой строки через compile или exec уже перехватывается:

try:
    compile("if True\n    pass", "<строка>", "exec")
except SyntaxError as exc:
    print(f"SyntaxError: {exc.msg}")
SyntaxError: expected ':'

Ошибки, на которых спотыкаются

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

Перехват ради тишины. except Exception: pass превращает падение в неправильный результат, и найти его потом дороже, чем починить исходную ошибку.

Проверка вместо перехвата и наоборот. Для редких сбоев дешевле перехват, для ожидаемых случаев дешевле проверка. Обращение к отсутствующему ключу словаря в цикле лучше решать через get, а не ловить KeyError миллион раз.

Исключение внутри except. Если обработчик сам падает, наружу уйдёт новая ошибка, а исходная останется только в трассировке как контекст. Код в except должен быть простым.

except Exception вместо конкретного типа. Ловит и опечатку в имени переменной, и битую сеть одинаково. NameError при этом выглядит как проблема с данными, хотя это баг в коде.

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

Чем TypeError отличается от ValueError

Первое означает неподходящий тип, второе — подходящий тип с негодным значением: int(None) даёт TypeError, int("десять")ValueError. Правило и таблица частых сообщений — в отдельном разборе.

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

Исключение — механизм сообщения о проблеме: интерпретатор или твой код создаёт и поднимает объект-исключение. Но логическая ошибка может просто дать неверный результат без исключения. Синтаксические ошибки тоже представлены исключениями, например SyntaxError, хотя возникают ещё до выполнения тела программы.

Как повторить код после ошибки

Оберни try/except в цикл и выйди из него через break, когда операция завершилась успешно:

while True:
    try:
        age = int(input("Возраст: "))
        break
    except ValueError:
        print("Введите число ещё раз")

Здесь повторяется только ожидаемый ValueError. Для сетевых запросов и записи данных ограничивай число попыток: бесконечный цикл может зависнуть или повторить операцию дважды. Если одна политика повтора нужна нескольким функциям, вынеси её в декоратор @retry.

Как узнать текст ошибки

Через as: except ValueError as exc, дальше str(exc) или прямо в f-строке. Тип берётся как type(exc).__name__, а полная трассировка — модулем traceback.

Нужен ли except, если есть finally

Нет, конструкция try/finally без except допустима. Она означает «убрать за собой в любом случае, но ошибку не глушить»: исключение уйдёт наружу, а finally перед этим отработает.

Как поймать несколько исключений разом

Кортежем: except (ValueError, TypeError) as exc. Если каждому типу нужна своя реакция, пишут несколько блоков except от узкого к широкому.

Что делает except* со звёздочкой

Ловит группы исключений, ExceptionGroup, где за раз прилетает несколько ошибок сразу. Такое бывает при параллельном выполнении задач: несколько из них падают одновременно, и обычный except увидел бы только группу целиком.

Где потренироваться

В пути «Python: валидаторы» на Koddo обработка ошибок разбирается вместе с проверкой входных данных: сначала перехват разбора числа, потом собственное исключение, потом сбор всех нарушений вместо падения на первом. Проверить себя можно на задаче про проверку пароля: там надо не упасть на первом же нарушении, а собрать их все.

Источники