try-except в Python: ловим только нужные исключения
Голый except: и except Exception глотают всё подряд — от опечатки в имени переменной до Ctrl+C. Разбираем, как ловить конкретные исключения и не прятать чужие ошибки.

Строка except: в блоке обработки ошибок ловит не только то, о чём вы думали. Она перехватывает KeyboardInterrupt (нажатый Ctrl+C), SystemExit (попытку завершить процесс) и NameError от вашей же опечатки в имени переменной внутри try. Результат: сервис не убивается по Ctrl+C, а падающий тест зеленеет, потому что реальную ошибку проглотил обработчик. Разбираем, как ловить ровно те исключения, которые вы готовы обработать, и почему except Exception — не то же самое, что except:. Всё ниже проверено на CPython 3.11 и 3.12; поведение групп исключений через except* появилось в 3.11.
Задача и почему очевидное решение подводит
Типичный сценарий: код обращается к внешнему API, вы хотите поймать сетевую ошибку и повторить запрос. Очевидное решение выглядит так:
# Python 3.11. requests 2.x
import requests
try:
response = requests.get("https://api.example.com/data", timeout=5)
data = response.json()
except Exception:
data = None # "на всякий случай" ловим всё
Проблема в том, что except Exception здесь прячет не только сетевой сбой. Если вы опечатались и написали respons.json(), интерпретатор бросит NameError — и обработчик молча вернёт None. Если response.json() упадёт на некорректном JSON, вы получите тот же None и не узнаете, что API отдал HTML со страницей ошибки. Ошибка проглочена, поведение непредсказуемо, а вы полдня ищете, почему data внезапно пустая.
Цена вопроса — не эстетика. Проглоченное исключение живёт в проде месяцами и всплывает как «иногда данные теряются». Отладка такого стоит дороже, чем аккуратный обработчик с самого начала.
Иерархия исключений: что от чего наследуется
Ключ к точной ловле — понимать, что все исключения выстроены в дерево. На вершине BaseException. От него наследуются Exception (обычные ошибки программы) и отдельно — KeyboardInterrupt, SystemExit, GeneratorExit. Именно поэтому except: и except BaseException: перехватывают Ctrl+C, а except Exception: — нет.
| Конструкция | Что ловит | Когда уместно |
|---|---|---|
except ValueError: | Только ValueError и его подклассы | Почти всегда — вы знаете, чего ждёте |
except (ValueError, KeyError): | Несколько конкретных типов | Когда разные ошибки обрабатываются одинаково |
except Exception: | Все ошибки программы, кроме Ctrl+C и SystemExit | Верхний уровень: логировать и продолжить |
except BaseException: / except: | Вообще всё, включая Ctrl+C | Почти никогда |
Ловите самое узкое исключение, которое действительно готовы обработать. Всё, что вы не ждали, должно долететь до логов и упасть — тихая ошибка дороже громкой.
Как переписать пример правильно
Разнесём то, что мы обрабатываем, и то, что должно всплыть. Сетевые ошибки в requests наследуются от requests.exceptions.RequestException — ловим именно её. Ошибку разбора JSON обрабатываем отдельно, а опечатки в коде пусть падают как NameError:
# Python 3.11. requests 2.x
import json
import logging
import requests
logger = logging.getLogger(__name__)
def fetch_data(url: str) -> dict | None:
try:
response = requests.get(url, timeout=5)
response.raise_for_status() # бросит HTTPError на 4xx/5xx
return response.json()
except requests.exceptions.RequestException as error:
# сеть, таймаут, DNS, код ответа 4xx/5xx
logger.warning("Запрос к %s не удался: %s", url, error)
return None
except json.JSONDecodeError as error:
# ответ пришёл, но это не JSON
logger.error("Ответ от %s не JSON: %s", url, error)
return None
Опечатка вроде respons.json() теперь бросит NameError, который не совпадает ни с одним из except, — и вы увидите её сразу. Именно это и нужно: обработчик ловит ожидаемое, всё остальное падает.
Не глушите исключение молча
Если внутри except вы ничего не логируете и не пробрасываете, восстановите исходную причину при новом исключении через raise ... from:
# Python 3.11
class DataUnavailable(Exception):
pass
try:
value = int(raw_value)
except ValueError as error:
# сохраняем цепочку: в трейсбеке будет видно исходный ValueError
raise DataUnavailable(f"Не смог разобрать {raw_value!r}") from error
Без from error Python всё равно покажет «During handling of the above exception», но from делает связь явной и убирает шум, если исходную ошибку показывать не нужно (from None).
Группы исключений: except* с версии 3.11
Если код параллельно выполняет несколько задач и падает сразу в нескольких местах (например, через asyncio.TaskGroup), одиночное исключение не описывает ситуацию. В Python 3.11 появились ExceptionGroup и синтаксис except*, который ловит подмножество исключений из группы:
# Python 3.11+
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(load_users())
tg.create_task(load_orders())
except* ConnectionError as eg:
# eg.exceptions — все ConnectionError из группы
logger.warning("Сетевые сбои: %s", eg.exceptions)
except* ValueError as eg:
logger.error("Ошибки данных: %s", eg.exceptions)
Если вы на 3.10 или ниже — except* недоступен и TaskGroup тоже; там придётся собирать результаты asyncio.gather(..., return_exceptions=True) и разбирать список вручную. Проверьте версию: python --version.
Где подход ломается
- Слишком широкий try. Если в
tryдесять строк, аexcept ValueErrorодин — вы не знаете, какая из строк его бросила. Оборачивайте минимум кода: строку-две, где ошибка ожидаема. - Порядок except важен. Обработчики проверяются сверху вниз. Если
except Exceptionстоит вышеexcept ValueError, до второго дело не дойдёт — он поймает всё первым. Узкие типы всегда выше широких. - Ловля в цикле съедает Ctrl+C. Широкий
exceptвнутри бесконечного цикла может проглотитьKeyboardInterrupt, если вы написалиexcept BaseExceptionили голыйexcept:. Процесс перестаёт останавливаться по Ctrl+C. - Логирование без трейсбека.
logger.error(str(error))теряет стек. Используйтеlogger.exception(...)внутриexcept— он приложит полный трейсбек автоматически.
Что делать дальше
Пройдитесь по проекту поиском голых except: и except Exception: — линтеры это ловят: у ruff и flake8-bugbear есть правило на слишком широкую ловлю (в ruff — код BLE001, точный набор правил сверяйте с документацией вашей версии ruff). На верхнем уровне сервиса оставьте один широкий except Exception, который логирует через logger.exception и не даёт процессу упасть, а внутри бизнес-логики ловите только конкретные типы. Инвалидация и повторные попытки (retry с backoff) — отдельная тема; здесь мы разбирали только точность ловли.
Сочинение: Тема, тезис, аргументы, готовый текст нужного объёма
Спросить за 12 ₽Источники: Python docs — Errors and Exceptions, Python docs — Built-in Exceptions (иерархия)
Частые вопросы
В чём разница между except: и except Exception:?
А если у меня Python 3.9 или 3.10 — except* сработает?
Что делать, если я не знаю, какое исключение бросит библиотека?
Можно ли поймать несколько типов исключений одним блоком?
Почему мой except ValueError не срабатывает, а ошибка всё равно падает?
Как логировать пойманное исключение, не теряя трейсбек?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.