Любая программа рано или поздно встречает то, чего не ожидала: файл не открылся, сеть отвалилась, на вход пришёл мусор. В Python такие ситуации описываются исключениями (exceptions), а освобождение ресурсов после них — контекстными менеджерами и оператором with. Разберёмся, как пользоваться и тем и другим, чтобы ошибки не терялись, а ресурсы не утекали.
Зачем вообще исключения
Можно было бы возвращать из каждой функции код ошибки и проверять его на каждом шаге. Так делают в некоторых языках, и код быстро тонет в проверках, а одну забытую проверку легко не заметить.
Python идёт другим путём. Когда что-то идёт не так, функция возбуждает (raise) исключение — особый объект, который прерывает обычный ход выполнения и «всплывает» вверх по стеку вызовов, пока его кто-нибудь не перехватит (except). Если не перехватил никто — программа падает с трассировкой (traceback), и это правильное поведение по умолчанию: лучше громко упасть, чем тихо продолжить с испорченными данными.
Короткая формула: исключения — для ситуаций, которые мешают функции выполнить свою работу. Не для обычного ветвления логики.
try / except: перехват
Базовая конструкция — try с одним или несколькими except:
def parse_age(raw: str) -> int:
try:
return int(raw) # может бросить ValueError
except ValueError:
return 0 # подставляем безопасное значение
Можно ловить несколько типов — по отдельности или группой:
try:
data = load_config(path)
except FileNotFoundError:
data = default_config() # файла нет — берём дефолт
except (PermissionError, IsADirectoryError) as err:
# одна ветка на несколько типов; err — сам объект исключения
raise RuntimeError(f"не удалось прочитать {path}") from err
Несколько важных мелочей:
as errдаёт доступ к объекту исключения — его сообщению, атрибутам.- Порядок
exceptважен: интерпретатор берёт первый подходящий. Более конкретные типы ставь выше более общих. raise ... from errсохраняет исходную причину в цепочке (в трассировке будет видно «The above exception was the direct cause»). Это помогает не терять контекст при «переупаковке» ошибки.
else и finallyспросят на собеседовании
У try есть ещё две ветки, про которые часто забывают.
else выполняется, только если в try не было исключения. Туда выносят код, который должен идти после успешной операции, но сам не должен попадать под except:
try:
conn = open_connection(url)
except ConnectionError:
log.warning("нет связи")
else:
# сюда попадаем, только если соединение реально открылось
use(conn)
finally выполняется всегда — и при успехе, и при исключении, и даже если внутри try сработал return. Это место для гарантированной уборки:
f = open("data.txt")
try:
process(f)
finally:
f.close() # закроется в любом случае
Короткая формула: else — «успех», finally — «в любом случае».
Иерархия исключенийспросят на собеседовании
Все исключения — это классы, и они выстроены в дерево наследования. Когда ты пишешь except SomeError, перехватываются и SomeError, и все его потомки.
Упрощённо вершина дерева выглядит так:
Ctrl+C и sys.exit() идут мимо Exception — поэтому except Exception их не перехватывает и программа спокойно завершается. Отсюда и правило: BaseException не ловят, иначе процесс перестанет откликаться на остановку.
От Exception уже идут знакомые типы: ValueError, TypeError, KeyError, OSError — а у него свои потомки, FileNotFoundError и PermissionError.
Практический вывод: лови Exception, а не BaseException. KeyboardInterrupt (нажатие Ctrl+C) и SystemExit наследуются напрямую от BaseException — их перехват ломает нормальное завершение программы. Обычный код их трогать не должен.
И ещё: лови как можно более узкий тип. except Exception ловит вообще всё подряд, включая ошибки, которых ты не ждал (опечатку в имени переменной выдаёт NameError — тоже потомок Exception). Чем уже тип, тем меньше шанс случайно проглотить чужую ошибку.
Не глотай ошибки
Самый частый и опасный антипаттерн:
# так делать НЕ надо
try:
risky()
except Exception:
pass # ошибка исчезла бесследно
Такой except ... : pass молча проглатывает всё. Программа продолжает работать с непредсказуемым состоянием, а причину сбоя потом не найти — трассировки нет.
Если перехватываешь исключение — сделай с ним что-то осмысленное:
import logging
log = logging.getLogger(__name__)
try:
send_metrics(payload)
except ConnectionError:
# метрики не критичны — логируем и идём дальше осознанно
log.warning("метрики не отправлены", exc_info=True)
exc_info=True добавит в лог полную трассировку. Если же ты не знаешь, что делать с ошибкой, — не лови её. Пусть всплывёт к тому, кто знает. Молчаливое подавление допустимо только тогда, когда ты сознательно решил, что эта ошибка здесь не важна, и оставил комментарий почему.
Свои исключения
Когда стандартных типов не хватает, заводят свои — наследуясь от Exception:
class OrderError(Exception):
"""Базовый класс для всех ошибок модуля заказов."""
class OrderNotFound(OrderError):
def __init__(self, order_id: int):
self.order_id = order_id
super().__init__(f"заказ {order_id} не найден")
Зачем это нужно:
- Общий базовый класс (
OrderError) позволяет вызывающему коду однимexcept OrderErrorпоймать все ошибки твоего модуля, не перечисляя их. - Собственные атрибуты (
order_id) передают данные об ошибке, а не только текст.
Возбуждают исключение оператором raise:
def get_order(order_id: int) -> Order:
order = repo.find(order_id)
if order is None:
raise OrderNotFound(order_id)
return order
Короткая формула: свои исключения = свой словарь ошибок предметной области. Они делают код-вызыватель чище и устойчивее.
Контекстные менеджеры и withспросят на собеседовании
Вернёмся к finally с f.close(). Это настолько частый случай, что в Python есть отдельный синтаксис — оператор with:
with open("data.txt") as f:
process(f)
# здесь файл уже гарантированно закрыт — даже при исключении внутри
with берёт контекстный менеджер (объект, который умеет «входить» в блок и «выходить» из него), вызывает его настройку на входе и уборку на выходе. Выход срабатывает при любом исходе — нормальном завершении блока, return или исключении. По сути это try/finally, упакованный в одну строку.
Файлы — самый известный пример, но так же работают блокировки потоков, соединения с БД, транзакции:
import threading
lock = threading.Lock()
with lock: # lock.acquire() на входе
update_shared_state()
# lock.release() на выходе — даже если update_shared_state бросит
Можно открыть несколько менеджеров сразу:
with open("in.txt") as src, open("out.txt", "w") as dst:
dst.write(src.read())
Как написать свой контекстный менеджерспросят на собеседовании
Контекстным менеджером объект делают два метода: __enter__ (вызывается на входе в with, его результат попадает в as-переменную) и __exit__ (вызывается на выходе):
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self # это значение попадёт в `as`
def __exit__(self, exc_type, exc_value, traceback):
elapsed = time.perf_counter() - self.start
print(f"заняло {elapsed:.3f} c")
return False # False — исключение не подавляем
with Timer():
do_heavy_work()
В __exit__ приходят три аргумента с информацией об исключении (если блок завершился ошибкой) — или три None, если всё прошло чисто. Если __exit__ вернёт True, исключение считается обработанным и не всплывает дальше; False (или None) — оно пробрасывается как обычно. Подавлять стоит только осознанно.
Часто писать класс ради этого избыточно. В стандартной библиотеке есть contextlib.contextmanager — декоратор, который превращает генератор в контекстный менеджер. Код до yield — это «вход», код после — «выход»:
from contextlib import contextmanager
@contextmanager
def opened(path: str):
f = open(path)
try:
yield f # значение для `as`; здесь идёт тело with
finally:
f.close() # гарантированная уборка
with opened("data.txt") as f:
process(f)
yield тут ровно один. Обрати внимание на try/finally вокруг него — он и обеспечивает уборку при исключении внутри блока. Это самый компактный способ написать одноразовый менеджер ресурса.
Глубже: группы исключений, add_note и contextlibрасширенное
Внутри except голый raise пробрасывает то же исключение с исходной трассировкой. raise err
делает почти то же, но добавляет в трассировку лишнюю строку, а raise NewError(...) from err
нужен, когда ошибку осознанно переупаковывают. С версии 3.11 к исключению можно приписать
контекст, не заворачивая его: err.add_note(f"заказ {order_id}") — заметка появится в
трассировке под сообщением.
Там же появились группы исключений: когда несколько задач работали параллельно и упали
две из них, одна ошибка не может быть «причиной» другой. asyncio.TaskGroup поднимает
ExceptionGroup, а разбирают её конструкцией except*, которая ловит из группы ошибки
нужного типа и оставляет остальные всплывать дальше:
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(fetch_prices())
tg.create_task(fetch_stock())
except* TimeoutError as group:
log.warning("не дождались: %d задач", len(group.exceptions))
Модуль contextlib закрывает два частых случая. suppress глотает ошибку явно и видно для
читателя, в отличие от except: pass:
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("cache.tmp") # нет файла — и не надо
А ExitStack держит заранее неизвестное число менеджеров: открыть все файлы из списка и
гарантированно закрыть каждый, сколько бы их ни было.
Глубже: файлы и сериализация: open, json и pickleрасширенноеспросят на собеседовании
Файловый объект, который возвращает open, сам контекстный менеджер: with open(path, encoding="utf-8") as f закрывает файл при любом выходе из блока, и это единственная правильная форма, потому что незакрытый файл держит дескриптор операционной системы, а буфер записи может не доехать до диска. Режим задают строкой: "r" чтение, "w" запись с обнулением, "a" дописывание, буква b переключает в двоичный режим, где читаются bytes, а не str. Кодировку для текста указывают всегда: без неё она берётся из системы, и файл, записанный на одной машине, ломается на другой.
Сериализация это перевод объекта в байты или текст, чтобы отправить его по сети или положить на диск. Для обмена с другими системами берут json: json.dumps даёт строку, json.dump пишет в файл, парные loads и load читают обратно. Понимает он только словари, списки, строки, числа, булевы и None; дату, Decimal или свой класс надо превратить во что-то из этого списка самим, либо передать default=.
живой пример
import json
from dataclasses import asdict, dataclass
from datetime import date
@dataclass
class Order:
id: int
created: date
text = json.dumps(asdict(Order(42, date(2026, 10, 5))), default=str, ensure_ascii=False)
print(text) # {"id": 42, "created": "2026-10-05"}
print(json.loads(text)["id"] + 1) # 43
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
pickle сериализует почти любой объект Python вместе с его классом, но формат понимает только Python, и, главное, при загрузке он выполняет код: pickle.loads на данных из недоверенного источника равносилен запуску чужой программы. Поэтому pickle остаётся для кеша и обмена между своими процессами, а наружу уходит json, Protobuf или Avro.
Коротко
- Исключения — для ситуаций, которые мешают функции сделать работу; не для обычного ветвления.
try/exceptперехватывает;else— ветка успеха;finally— выполняется всегда, место для уборки.- Лови узкий тип и
Exception, но неBaseException— иначе перехватишь Ctrl+C иSystemExit. except ...: pass— антипаттерн: либо обработай и залогируй (exc_info=True), либо не лови вообще.- Свои исключения с общим базовым классом и атрибутами дают чистый «словарь ошибок» предметной области; возбуждаются через
raise, причину сохраняетraise ... from err. withгарантирует уборку ресурса при любом исходе; свой менеджер — это__enter__/__exit__или генератор под@contextmanager.except*разбираетExceptionGroupот параллельных задач,add_noteдобавляет контекст,contextlib.suppressглотает ошибку явно, аExitStackдержит много менеджеров сразу.- Файл открывают только через
with open(..., encoding="utf-8"); наружу данные уходят черезjsonсdefault=для дат,pickleтолько для своих процессов, потому чтоloadsвыполняет код.
Что почитать дальше
- Функции и модули — как организовать код, в котором живут твои исключения и менеджеры.
- Итераторы и генераторы — тот самый
yield, на котором держится@contextmanager. - Инструменты разработчика — линтеры и форматтеры, которые ловят проглоченные исключения и прочие огрехи.