Пользовательские исключения в Python — руководство
О чём эта статья

Эта подробная инструкция объясняет, зачем нужны пользовательские (кастомные) исключения в Python, как их правильно определять, возбуждать, обрабатывать и группировать. Вы найдёте практические рекомендации, шаблоны, чек-листы для ролей и примеры распространённых ошибок при использовании пользовательских исключений.
Важно: статья ориентирована на разработчиков и командных инженеров. Примеры даны на Python 3.x.
Когда встроенных исключений недостаточно

Стандартные исключения Python (ValueError, TypeError, FileNotFoundError и т. п.) покрывают множество сценариев, но не всегда отражают прикладную суть ошибки в вашем приложении. Когда в логах и обработчиках требуется семантика уровня предметной области (domain), полезно ввести собственные типы ошибок:
- чтобы отделить ошибки валидации пользователя от внутренних ошибок сервиса;
- чтобы группировать все ошибки, связанные с внешним API, в одну иерархию;
- чтобы сделать код обработчиков понятным без изучения реализации.
Простой пример: как определить пользовательское исключение
Создавайте исключения небольшими и чистыми — храните только те атрибуты, которые действительно понадобятся обработчикам.
class MyCustomError(Exception):
def __init__(self, message=None):
self.message = message
super().__init__(message)Этот класс принимает необязательное сообщение. Вызов super().init(message) сохраняет стандартное поведение Exception (строка и стек).
Как возбуждать (raise) пользовательские исключения
Чтобы вызвать исключение, используйте raise с экземпляром класса или с самим классом (короткая форма для исключений без аргументов):
if True:
raise MyCustomError("A Custom Error Was Raised!!!.")
# Или без аргументов
if True:
raise MyCustomErrorОбычно рекомендуется всегда передавать сообщение и, при необходимости, структурированные поля (код ошибки, контекст) — это облегчает логирование и диагностику.

Обработка пользовательских исключений
Обработка ошибок ничем не отличается от работы с встроенными исключениями: try/except/finally остаётся стандартной конструкцией.
try:
print("Hello, You're learning how to MakeUseOf Custom Errors")
raise MyCustomError("Opps, Something Went Wrong!!!.")
except MyCustomError as err:
print(f"Error: {err}")
finally:
print("Done Handling Custom Error")Если исключение не будет перехвачено соответствующим except, после выполнения finally оно снова поднимется и может привести к аварийному завершению программы.
try:
raise KeyboardInterrupt
except MyCustomError as err:
print(f"Error: {err}")
finally:
print("Did not Handle the KeyboardInterrupt Error. Can Only Handle MyCustomError")В этом примере KeyboardInterrupt не перехвачен, выполняется finally, затем исключение поднимается дальше.

Наследование и групповая обработка ошибок

Для комплексных приложений имеет смысл построить иерархию исключений. Создайте базовый класс для семантически связанных ошибок, затем — более узкие подклассы.
Пример: ошибки, связанные с вызовами внешнего API.
class BaseAPIException(Exception):
"""Base class for API-related exceptions."""
def __init__(self, message):
super().__init__(message)
self.message = message
class APINotFoundError(BaseAPIException):
"""Raised when the requested resource is not found in the API."""
pass
class APIAuthenticationError(BaseAPIException):
"""Raised when there's an issue with authentication to the API."""
pass
class APIRateLimitExceeded(BaseAPIException):
"""Raised when the rate limit for API requests is exceeded."""
passИх удобно перехватывать как по конкретному типу, так и по базовому классу, чтобы обрабатывать общие случаи:
try:
# Симуляция ошибки API
raise APINotFoundError("Requested resource not found.")
except APINotFoundError as err:
print(f"API Not Found Error: {err}")
except APIAuthenticationError as err:
print(f"API Authentication Error: {err}")
except APIRateLimitExceeded as err:
print(f"API Rate Limit Exceeded: {err}")
except BaseAPIException as err:
print(f"Unknown API Exception: {err}")Последний except работает как catch-all для ошибок, связанных с API.
Оборачивание (wrapping) исключений для контекста

Оборачивание — это приём, когда вы ловите низкоуровневое исключение, добавляете контекст и поднимаете своё исключение, сохранив ссылку на исходное через from.
def request_api():
try:
# Предположим, внешний код выбросил LookupError
raise LookupError("Sorry, You Encountered A LookUpError !!!")
except LookupError as original_exception:
try:
# Оборачиваем оригинальную ошибку собственным типом
raise APINotFoundError("Requested resource not found.") from original_exception
except APINotFoundError as wrapped_exception:
print(f"Caught wrapped API exception: {wrapped_exception}")
raise
try:
request_api()
except APINotFoundError as err:
print(f"Caught API exception: {err.__cause__}")Атрибут cause позволяет отследить первопричину. Это особенно полезно при логировании и в отладочных страницах ошибок.
Когда пользовательские исключения НЕ работают
- Если ваша библиотека предназначена для низкоуровневого кода (язык/парсер), стандартные Exception-подклассы могут быть предпочтительнее.
- Если API широко используется сторонними библиотеками, добавление новых публичных типов исключений требует поддерживаемой семантики назад-совместимости.
- Не стоит создавать слишком мелкие типы исключений, которые затем никем не перехватываются — это усложнит код.
Альтернативные подходы
- Использовать коды ошибок/enum в возвращаемых значениях вместо исключений (подходит для high-throughput кода, где исключения дороги по производительности).
- Возвращать Result-объект (patent from functional languages) — например, NamedTuple с полями success/data/error.
- Для внешних API использовать библиотечные адаптеры, которые переводят сторонние исключения в вашу иерархию.
Практические правила и соглашения (Best practices)
- Именуйте классы исключений с суффиксом Error: MyDomainError, APIAuthenticationError.
- Всё прикладное пространство ошибок группируйте под одним корневым классом (например, MyAppError).
- Храните минимум состояний в исключении: message, code (если нужно), context (словарь с небольшой диагностикой).
- Логируйте исключения с полным стеком в месте, где вы принимаете решения о retry/rollback.
- Не используйте исключения для обычного потока управления (avoid using exceptions for flow control).
Чек-листы по ролям
Для разработчика, пишущего фичу
- Есть ли смысл в отдельном типе исключения для этой ошибки?
- Исключение-наследник оформлен от базового класса приложения.
- Передаётся диагностическая информация (message, context).
- Написаны unit-тесты, проверяющие, что исключение выбрасывается в нужных местах.
Для инженера поддержки / техлида
- [ ] Логи содержат cause и контекст.
- Исключения, которые нужны для мониторинга, промаркированы (code/labels).
- Есть обработчики, выполняющие retry/rollback там, где это безопасно.
Критерии приёмки (пример)
- Новое исключение документировано в README и CHANGELOG.
- Юнит-тесты покрывают положительные и негативные сценарии.
- При интеграционных тестах исключение корректно сериализуется в лог.
- Обработчики приложения (middleware) не ломаются при добавлении нового типа исключения.
Unit-тесты и кейсы приёмки
Примеры тестов, которые стоит написать:
- Проверить, что при определённых входных данных возбуждается конкретный тип исключения.
- Проверить, что исключение содержит поле message и ожидаемый текст.
- Проверить, что при оборачивании cause ссылается на исходную ошибку.
- Проверить, что middleware приложения возвращает правильный HTTP-код для данного исключения.
Шаблон: SOP для добавления нового исключения
- Обсудить необходимость с командой (соглашения по именованию и иерархии).
- Создать класс в модуле errors/ или exceptions/.
- Добавить тесты (unit + интеграционные при необходимости).
- Обновить документацию и CHANGELOG.
- При релизе отметить обратную совместимость.
Краткие сниппеты-справочники (cheat sheet)
Определение простого исключения:
class MyDomainError(Exception):
"""Ошибки предметной области приложения."""
def __init__(self, message: str, code: str = None, context: dict = None):
self.message = message
self.code = code
self.context = context or {}
super().__init__(message)Оборачивание:
try:
external_call()
except SomeLibError as e:
raise MyDomainError("Не удалось выполнить external_call", code="EXT_CALL_FAIL") from eЛогирование с трейсом:
import logging
logger = logging.getLogger(__name__)
try:
do_work()
except MyDomainError:
logger.exception("Ошибка предметной области: %s", exc_info=True)
raiseМентальные модели и эвристики
- Ошибка — это не только сообщение: это контракт между уровнем, где ошибка возникла, и уровнем, который её будет обрабатывать.
- Используйте иерархии исключений как «фильтры»: от общего к частному.
- Оборачивайте системные ошибки (IOError, LookupError) в доменные ошибки на границе инфраструктуры.
Примеры ошибок и когда их выбирать
- ValueError / TypeError — при нарушении ожиданий по типам и значениям;
- MyDomainError — когда ошибка входит в предметную область (недостаточно общая для ValueError);
- BaseAPIException и его наследники — при работе с внешними API.
Маленький глоссарий (1 строка каждый)
- Исключение: объект, сигнализирующий о ненормальной ситуации в программе.
- Оборачивание (wrapping): перевод низкоуровневого исключения в доменное с сохранением причины.
- cause: атрибут исключения, указывающий на оригинальную ошибку при использовании from.
Decision flow (простое дерево решения)
flowchart TD
A[Произошла ошибка] --> B{Это ошибка уровня библиотеки?}
B -- Да --> C[Не оборачивать, использовать стандартное исключение]
B -- Нет --> D{Связанa ли ошибка с предметной областью?}
D -- Да --> E[Создать/использовать доменное исключение]
D -- Нет --> F[Использовать общие Exception или возврат Result]
E --> G{Нужна ли иерархия?}
G -- Да --> H[Создать базовый класс и подклассы]
G -- Нет --> I[Создать одиночный тип исключения]Совместимость, миграции и заметки по API
- Если ваше исключение становится частью публичного API, его изменение — breaking change. Планируйте версии и депрекейт-коммуникацию.
- Для миграции из строковых кодов ошибок на типы — добавьте пакет-адаптер, который переводит старые коды в новые исключения.
Частые ошибки и анти-паттерны
- Создание сотен микроскопических типов исключений, которые никто не перехватывает.
- Хранение большого объёма данных (большие объекты) в атрибутах исключения — это может привести к утечкам памяти.
- Проглатывание исключений без логирования (silent fail).
Заключение
Пользовательские исключения — мощный инструмент для управления ошибками на уровне предметной области. Простая иерархия, разумная степень детализации и аккуратное оборачивание внешних ошибок делают код более понятным и удобным для поддержки.
Ключевые рекомендации:
- Держите исключения простыми и предсказуемыми.
- Используйте наследование для группирования.
- Логируйте контекст и первопричину.

Похожие материалы
Градиенты в Canva: добавить и настроить
Ошибка Disabled accounts can't be contacted в Instagram
Генерация случайных чисел в Google Sheets
Прокручиваемые скриншоты в Windows 11
Как установить корпусной вентилятор в ПК