Четыре блока
try:
data = load(path) # опасный код — как можно короче
except FileNotFoundError:
data = default() # обработка конкретной ошибки
else:
validate(data) # только если ошибки не было
finally:
log.info("done") # всегда
else полезен тем, что исключения из него не попадают в except выше: так обработка не маскирует ошибки в коде, который к ней не относится.
Ловить конкретное
except: без типа перехватит даже KeyboardInterrupt и SystemExit. except Exception — тоже слишком широко, если вы не логируете и не пробрасываете ошибку дальше. Ловят то, что умеют обработать, остальное пусть летит выше.
Свои исключения
У проекта или библиотеки — один базовый класс и иерархия под ним:
class PaymentError(Exception): ...
class CardDeclined(PaymentError): ...
class ProviderUnavailable(PaymentError): ...
Вызывающему удобно: можно поймать всё семейство except PaymentError или конкретный случай.
Цепочки исключений
Когда низкоуровневую ошибку превращают в понятную, причину сохраняют:
try:
resp = client.post(url, json=payload, timeout=5)
except httpx.TimeoutException as err:
raise ProviderUnavailable("платёжный шлюз не ответил") from err
from err кладёт исходную ошибку в __cause__, и в трассировке видно обе. Простой raise внутри except тоже связывает их через __context__, но с пометкой «во время обработки возникла другая ошибка». from None скрывает исходную — уместно редко. С Python 3.11 к исключению можно добавить контекст без обёртки: err.add_note("заказ 42").
Контекстные менеджеры
with гарантирует подготовку и очистку вокруг блока, даже при исключении:
with open("data.csv") as f:
rows = f.readlines()
# файл закрыт, что бы ни случилось
Протокол — __enter__ при входе и __exit__ при выходе. Проще написать менеджер генератором:
from contextlib import contextmanager
@contextmanager
def timer(name):
start = time.perf_counter()
try:
yield
finally:
print(name, time.perf_counter() - start)
Если __exit__ вернёт True, исключение будет подавлено — так делают редко и осознанно (contextlib.suppress). Ресурсы — файлы, соединения, блокировки, транзакции — закрывают через with, а не через __del__.
Несколько ошибок сразу
В конкурентном коде могут упасть несколько задач. asyncio.TaskGroup (3.11) отменяет остальные задачи при первой ошибке и собирает ошибки в ExceptionGroup, а except* разбирает группу по типам:
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(fetch_a())
tg.create_task(fetch_b())
except* httpx.TimeoutException as group:
log.warning("таймаутов: %d", len(group.exceptions))
asyncio.gather ведёт себя иначе: пробрасывает первую ошибку, а остальные задачи продолжают работать брошенными.
Что запомнить
try— короткий,except— конкретный,else— код после успеха.- Свои исключения — иерархия от одного базового класса.
- Перевод ошибки —
raise ... from err. - Ресурсы — через
with. - TaskGroup отменяет соседей и возвращает ExceptionGroup.