Гид по технологиям

Пользовательские исключения в Python: как создавать, обрабатывать и проектировать

5 min read Python Обновлено 29 Dec 2025
Пользовательские исключения в Python
Пользовательские исключения в 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.
  • Если у вас нет плана обработки разных типов ошибок — не создавайте лишних типов.

Практическая методология разработки исключений (мини-метод)

  1. Определите доменные ошибки: какие сбои важны для владельца фичи.
  2. Создайте базовый класс для домена (например, BaseAPIException).
  3. Для каждого важного сценария создайте подкласс с понятным именем.
  4. Добавьте поля, которые понадобятся для логирования/мониторинга (request_id, code).
  5. Документируйте исключения в API/README.
  6. Напишите приемочные тесты и 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)})
    raise

Decision 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 для сохранения причины.
  • Документируйте и тестируйте поведение исключений.
Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Несколько аккаунтов Skype: Multi Skype Launcher
Программное обеспечение

Несколько аккаунтов Skype: Multi Skype Launcher

Журнал для работы: повысить продуктивность
Productivity

Журнал для работы: повысить продуктивность

Персональные звуки уведомлений на Android
Android.

Персональные звуки уведомлений на Android

Скачивание шоу Hulu для офлайн‑просмотра
Стриминг

Скачивание шоу Hulu для офлайн‑просмотра

Microsoft Start: персонализированная новостная лента
Новости

Microsoft Start: персонализированная новостная лента

Как изменить имя в Epic Games быстро
Гайды

Как изменить имя в Epic Games быстро