Пользовательские исключения в Python: как создавать, обрабатывать и проектировать
TL;DR
Пользовательские исключения помогают сделать ошибки точными, понятными и удобными для отладки. Создавайте простые классы исключений, наследуйте их для группировки ошибок, «оборачивайте» встроенные исключения через raise … from, тестируйте и документируйте — и у вас будет надёжная модель ошибок.

Python предоставляет базовые классы исключений, но в реальных приложениях иногда нужно описать уникальные ситуации — например, ошибки доменной логики, ошибки интеграции с API или валидации входных данных. Пользовательские исключения (custom exceptions) позволяют передать контекст и контролировать потоки управления гибко и читаемо.
Что такое пользовательские исключения и зачем они нужны
Пользовательское исключение — это класс, обычно унаследованный от Exception или от одного из её подклассов, который представляет собой конкретную ошибку в вашей предметной области.
Коротко:
- Повышают читабельность кода; вместо ловли общего Exception вы ловите понятное по имени исключение.
- Позволяют приоритизировать обработку ошибок (вложенность except от более специфичного к более общему).
- Упрощают локализацию сообщений и сопроводительных данных об ошибке.
Важно отметить: не стоит создавать исключения для каждой ошибки — используйте их для значимых, повторяемых или для тех, которые требуют отдельной реакции.
Базовый шаблон: как определить собственное исключение
Правило: держите классы исключений простыми. Храните в них данные, которые понадобятся обработчику.
Пример простого пользовательского исключения:
class MyCustomError(Exception):
def __init__(self, message=None):
self.message = message
super().__init__(message)Ключевые моменты:
- Наследование от Exception даёт стандартное поведение.
- Хранение message как атрибута упрощает доступ без разбора строки.
- Можно добавлять дополнительные поля (код ошибки, payload, request_id).
Как выбрасывать (raise) исключения
Используйте ключевое слово raise с экземпляром класса или с классом (короткая запись не рекомендуема для передачи сообщения):
if True:
raise MyCustomError("A Custom Error Was Raised!!!.")
# или короткая форма, но она не инстанцирует исключение
if True:
raise MyCustomErrorПрактическая рекомендация: предпочитайте явную передачу аргументов (сообщения, дополнительных полей) — это улучшает отладку и логирование.
Как обрабатывать пользовательские исключения
Логика обработки не отличается от обычных исключений: try/except/finally.
try:
print("Hello, you're learning custom errors")
raise MyCustomError("Oops, 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 не перехватывается блоком except, поэтому после выполнения finally исключение снова поднимается.
Наследование пользовательских исключений
Наследование позволяет группировать ошибки по доменам и уровню абстракции.
Пример: базовое исключение для API и его специализации:
class BaseAPIException(Exception):
"""Базовый класс для ошибок, связанных с API."""
def __init__(self, message):
super().__init__(message)
self.message = message
class APINotFoundError(BaseAPIException):
"""Запрашиваемый ресурс не найден."""
pass
class APIAuthenticationError(BaseAPIException):
"""Проблема с аутентификацией в API."""
pass
class APIRateLimitExceeded(BaseAPIException):
"""Превышен лимит запросов к API."""
passОбработка ошибок может выглядеть так:
def request_api():
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 перехватывает все остальные ошибки, унаследованные от BaseAPIException.
Оборачивание (wrapping) исключений
Оборачивание — это практика перехвата низкоуровневого исключения и поднятия более высокоуровневого пользовательского исключения с сохранением причины через конструкцию raise … from.
Пример:
def request_api():
try:
# Предположим, внешний API вызвал LookupError
raise LookupError("External lookup failed")
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 cause: {err.__cause__}")Использование оператора from связывает новое исключение с исходным через атрибут cause, что облегчает трассировку и логирование.
Кастомизация поведения класса исключения
Класс исключения может реализовать дополнительные методы и dunder-методы (str, repr, iter), хранить контекст (request_id, payload), и поддерживать сериализацию для логов:
class ValidationError(Exception):
def __init__(self, message, field=None, code=None):
self.field = field
self.code = code
super().__init__(message)
def __str__(self):
base = super().__str__()
if self.field:
return f"{base} (field={self.field})"
return baseТакой класс даёт возможность логировать field и code отдельно, а не парсить строку сообщения.
Когда НЕ стоит создавать пользовательское исключение
- Если ситуация тривиальна и уже покрывается встроенным исключением (ValueError, TypeError).
- Когда ошибка локальна и не влияет на уровень модуля или сервиса — тогда достаточно вернуть значение ошибки или None.
- Если у вас нет плана обработки разных типов ошибок — не создавайте лишних типов.
Практическая методология разработки исключений (мини-метод)
- Определите доменные ошибки: какие сбои важны для владельца фичи.
- Создайте базовый класс для домена (например, BaseAPIException).
- Для каждого важного сценария создайте подкласс с понятным именем.
- Добавьте поля, которые понадобятся для логирования/мониторинга (request_id, code).
- Документируйте исключения в API/README.
- Напишите приемочные тесты и unit-тесты на сериализацию/сообщение/атрибуты.
Role-based чеклист при работе с исключениями
Разработчик:
- Создал базовый класс домена.
- Добавил полезные поля (context, id).
- Написал unit-тесты.
DevOps/Инженер SRE:
- Настроил алерты по типам исключений.
- Проверил, что cause логируется.
Поддержка/QA:
- Ознакомилась с именами исключений и сценарием их возникновения.
- Подготовила тест-кейсы для регрессионного тестирования.
Тесты и критерии приёмки
Критерии приёмки:
- Исключения имеют понятные имена и документацию.
- В каждом классе есть unit-тест, проверяющий message и дополнительные атрибуты.
- При оборачивании оригинальная причина сохраняется в cause.
- Логи содержат необходимые поля (timestamp, error_type, request_id).
Примеры тест-кейсов:
- Поднять APINotFoundError и убедиться, что err.cause — LookupError.
- Проверить, что ValidationError.str содержит имя поля при переданном field.
Примеры шаблонов и сниппетов
Шаблон структуры исключений для модуля “payments”:
class PaymentsError(Exception):
"""Базовый класс для ошибок payments."""
pass
class PaymentValidationError(PaymentsError):
pass
class PaymentGatewayError(PaymentsError):
def __init__(self, message, gateway_code=None):
self.gateway_code = gateway_code
super().__init__(message)Сниппет логирования исключения с контекстом:
import logging
logger = logging.getLogger(__name__)
try:
process_payment()
except PaymentsError as e:
logger.exception("Payment failed", extra={"error_type": type(e).__name__, "gateway_code": getattr(e, "gateway_code", None)})
raiseDecision flow (Mermaid)
flowchart TD
A[Произошла ошибка] --> B{Относится к домену?}
B -- Да --> C[Создать/поднять пользовательское исключение]
B -- Нет --> D[Использовать встроенное исключение]
C --> E{Нужно обернуть оригинал?}
E -- Да --> F[raise CustomError from original]
E -- Нет --> G[raise CustomError]
D --> H[Обработать/логировать]
F --> H
G --> HМодель зрелости обработки ошибок (уровни)
- Уровень 0 — нет стандартов: ошибки логируются как строки.
- Уровень 1 — используются встроенные исключения правильно (ValueError и т.п.).
- Уровень 2 — введены доменные пользовательские исключения и базовые поля.
- Уровень 3 — оборачивание, единый формат логов, алерты по типам исключений.
Риски и рекомендации по смягчению
- Риск: слишком большое число типов усложняет код.
- Смягчение: придерживайтесь уровня зрелости 2–3; группируйте ошибки через базовые классы.
- Риск: утечка внутренних данных в сообщениях об ошибке.
- Смягчение: фильтровать PII в str и в логах; хранить подробности в скрытых полях.
1‑строчная глоссарий
- Пользовательское исключение — класс, описывающий доменную ошибку.
- Оборачивание — encapsulation ошибки через raise … from.
- Base exception — общий предок для группы ошибок.
Документирование и совместимость
Опишите в README вашего модуля базовый набор исключений, примеры обработки и рекомендации для клиентов библиотеки. Если вы меняете сигнатуру исключения (добавили обязательный аргумент), это breaking change — задокументируйте в релиз-нотс.
Заключение
Пользовательские исключения — инструмент проектирования кода, который делает ошибки выразительнее и упрощает обработку исключительных ситуаций. Создавайте их осознанно, документируйте и тестируйте. Правильная модель исключений повышает надежность и облегчит сопровождение кода.
Важно: всегда думайте о безопасности сообщений об ошибках — не раскрывайте внутренние детали и персональные данные в сообщениях, отправляемых пользователю или в логах с открытым доступом.
Краткое резюме для быстрого старта:
- Определите базовый класс для домена.
- Создавайте специфичные подклассы для разных сценариев.
- Используйте raise … from для сохранения причины.
- Документируйте и тестируйте поведение исключений.
Похожие материалы
Несколько аккаунтов Skype: Multi Skype Launcher
Журнал для работы: повысить продуктивность
Персональные звуки уведомлений на Android
Скачивание шоу Hulu для офлайн‑просмотра
Microsoft Start: персонализированная новостная лента