Исключения: try, except, else, finally

⏱ 35 мин · 🧩 заданий: 5

Что такое исключение

Исключение — сигнал «что-то пошло не так», который прерывает обычное выполнение и поднимается вверх по стеку вызовов, пока его кто-нибудь не перехватит. Если не перехватит никто — программа завершится и напечатает трассировку (traceback).

def parse_age(text):
    return int(text)

def register(form):
    age = parse_age(form["age"])
    return f"Возраст {age}"

print(register({"age": "двадцать"}))

Как читать traceback

Читайте снизу вверх: последняя строка — тип и сообщение ошибки (ValueError: invalid literal...). Выше — цепочка вызовов, от внешнего к месту ошибки. Ищите первую строку из своего кода.

try / except

def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("  делить на ноль нельзя")
        return None
    return result

print(divide(10, 2))
print(divide(1, 0))

Несколько типов и объект исключения

def get_item(data, key):
    try:
        return data[key]
    except (KeyError, IndexError) as e:          # несколько типов в кортеже
        print(f"  нет элемента: {type(e).__name__}: {e}")
    except TypeError as e:
        print(f"  неверный тип ключа: {e}")
        print("  аргументы исключения:", e.args)

get_item({"a": 1}, "b")
get_item([1, 2], 5)
get_item([1, 2], "x")

Переменная e после блока except удаляется (чтобы не держать ссылку на трассировку).

Полная форма: else и finally

try:
    ...            # код, который может упасть
except SomeError:
    ...            # обработка ошибки
else:
    ...            # если исключения НЕ было
finally:
    ...            # ВСЕГДА — при успехе, ошибке, return, break
def process(value):
    print(f"process({value!r})")
    try:
        number = int(value)
    except ValueError:
        print("  except: не число")
    else:
        print(f"  else: успех, {number} * 2 = {number * 2}")
    finally:
        print("  finally: очистка")

process("21")
process("abc")

Пройдите пример 👣 Пошагово, чтобы увидеть, по каким веткам идёт выполнение.

Зачем else, если можно написать код в конце try? Чтобы не перехватить случайно исключения из этого кода: в try оставляют только то, что действительно может бросить ожидаемую ошибку.

finally и return

def tricky():
    try:
        return "из try"
    finally:
        print("finally выполняется перед выходом")

print(tricky())

def very_tricky():
    try:
        return "из try"
    finally:
        return "из finally"       # перекрывает return из try — так делать нельзя!

print(very_tricky())

return в finally

return (или break) в finally перекрывает и результат, и исключение — ошибка молча исчезнет. В Python 3.14 это предупреждение SyntaxWarning.

Иерархия исключений

Исключения — классы, и except ловит указанный класс и всех его наследников:

BaseException ├── SystemExit (sys.exit()) ├── KeyboardInterrupt (Ctrl+C) ├── GeneratorExit └── Exception ← от него наследуют все «обычные» ошибки ├── ArithmeticError → ZeroDivisionError, OverflowError ├── LookupError → KeyError, IndexError ├── ValueError → UnicodeDecodeError ... ├── TypeError ├── AttributeError, NameError ├── OSError → FileNotFoundError, PermissionError, TimeoutError, ConnectionError ├── RuntimeError → RecursionError, NotImplementedError ├── StopIteration └── ImportError → ModuleNotFoundError
print(KeyError.__mro__)
try:
    {}["x"]
except LookupError as e:             # ловит и KeyError, и IndexError
    print("LookupError поймал", type(e).__name__)

try:
    open("/нет/такого/файла.txt")
except OSError as e:
    print(type(e).__name__, e.errno, isinstance(e, FileNotFoundError))

Порядок except важен

Проверяются сверху вниз, срабатывает первый подходящий. Сначала конкретные исключения, потом общие: except Exception перед except ValueError сделает второй блок мёртвым кодом.

Чего не делать

def bad():
    try:
        reslt = 10 / 2
        return result            # опечатка: NameError
    except:                      # голый except проглотит и её
        return "что-то не так"

print(bad())                     # ошибка спрятана, искать её будете долго
Антипаттерн Почему плохо Как правильно
except: / except BaseException: ловит Ctrl+C, SystemExit, опечатки конкретные типы
except Exception: pass ошибки исчезают бесследно хотя бы logger.exception(...)
огромный блок try непонятно, что именно упало минимум кода в try
исключения для обычной логики в горячем цикле дорого проверка if, dict.get

except Exception допустим на верхнем уровне (обработчик запроса, главный цикл воркера) — чтобы залогировать неожиданную ошибку и не уронить весь сервис.

raise: бросить или пробросить

def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError(f"сумма должна быть положительной, получено {amount}")
    if amount > balance:
        raise ValueError("недостаточно средств")
    return balance - amount

try:
    withdraw(100, 500)
except ValueError as e:
    print("Ошибка:", e)

def load(path):
    try:
        raise FileNotFoundError(path)
    except FileNotFoundError:
        print("  логируем и пробрасываем дальше")
        raise                       # голый raise — то же исключение с исходным traceback

try:
    load("config.yaml")
except FileNotFoundError as e:
    print("наверху:", repr(e))

EAFP на практике

import os

# LBYL: проверка и действие разделены — между ними файл могут удалить
path = "settings.ini"
if os.path.exists(path):
    print("читаю")

# EAFP: просто пробуем
try:
    with open(path) as f:
        print(f.read())
except FileNotFoundError:
    print("файла нет — используем настройки по умолчанию")

Вопрос на собеседовании

«Зачем нужны else и finally в try?» — else выполняется, только если в try не было исключения: туда выносят код, ошибки которого не надо перехватывать этим except. finally выполняется всегда — для освобождения ресурсов (хотя чаще для этого используют with).

Итог

  • Исключение поднимается по стеку до первого подходящего except; traceback читают снизу вверх.
  • try/except/else/finally: else — при успехе, finally — всегда.
  • except ловит класс и наследников; сначала конкретные, потом общие.
  • Не пишите голый except, не глушите ошибки молча, держите try маленьким.
  • raise бросает новое исключение, голый raise пробрасывает текущее.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Напишите safe_int(text, default=0): возвращает int(text), а если строка не число (или передан None) — возвращает default. Перехватывайте только нужные исключения (ValueError и TypeError), без голого except.

Какие тесты проверяют код (3)
assert safe_int("42") == 42 and safe_int(" -7 ") == -7
assert safe_int("abc") == 0 and safe_int("3.5", default=-1) == -1
assert safe_int(None) == 0
Задание 2 Программа на Python

Напишите read_config(data, key): возвращает data[key] и в любом случае добавляет строку "прочитано" в список data["_log"] (используйте finally). Если ключа нет, KeyError должен вылететь наружу — не перехватывайте его.

Какие тесты проверяют код (2)
d = {"_log": [], "port": 80}; assert read_config(d, "port") == 80 and d["_log"] == ["прочитано"]
d = {"_log": []}
try:
    read_config(d, "host")
    ok = False
except KeyError:
    ok = True
assert ok and d["_log"] == ["прочитано"], "KeyError должен вылететь, но лог — записаться"

Задание 3 Один вариант ответа

Что выведет код?

def f():
    try:
        return "try"
    finally:
        print("finally")

print(f())
Задание 4 Заполнить таблицу

Выполнится ли блок?

выполнитсяне выполнится
else, если в try не было исключения
else, если в try было перехваченное исключение
finally, если исключение не перехвачено ни одним except
finally, если в try выполнился return
второй except, если подошёл первый
Отметьте ячейку — «+», пустая — «−».
Задание 5 Один вариант ответа

Почему except: без типа (и except BaseException:) считается плохой практикой?

Войдите, чтобы отмечать пройденные уроки.