Пользовательские исключения в Python

19 июня 2026
Автор

Антон Ларичев

Введение

Встроенные исключения Python — ValueError, TypeError, KeyError — покрывают большой круг стандартных ошибок. Однако в реальных приложениях часто возникают ситуации, которые стандартными исключениями точно не описать: ошибка валидации формы, нарушение бизнес-правила, недопустимое состояние объекта. Именно для таких случаев в Python предусмотрен механизм пользовательских исключений.

В этой статье вы узнаете, зачем нужны собственные классы исключений, как их правильно создавать, как строить иерархии ошибок и как применять их в блоках try/except на практических примерах.

Зачем создавать пользовательские исключения

Пользовательские исключения решают несколько важных задач:

  • Семантическая точность — вместо безликого ValueError код выбрасывает InvalidEmailError, и сразу понятно, что именно пошло не так.
  • Разделение ответственности — модуль сам определяет контракт своих ошибок, а вызывающий код может их ловить по типу.
  • Дополнительный контекст — к исключению можно прикрепить поле, код ошибки, исходное значение — всё, что нужно для диагностики и обработки.
  • Иерархия ошибок — можно поймать как конкретную ошибку, так и целый класс связанных ошибок одним except.

Наследование от класса Exception

Все пользовательские исключения должны наследоваться от Exception (или от одного из его подклассов). Наследование от базового BaseException не рекомендуется: он является корнем дерева всех исключений Python, включая системные (SystemExit, KeyboardInterrupt), которые прерывать не следует.

Минимальный пример — пустой класс с говорящим именем:

# Минимальное пользовательское исключение
class AppError(Exception):
    pass

Этого уже достаточно, чтобы выбросить и поймать исключение:

raise AppError("Что-то пошло не так")

Добавление атрибутов и сообщений через init

Чтобы исключение несло больше информации, переопределите метод __init__. Это позволяет передавать произвольные данные вместе с ошибкой.

class ValidationError(Exception):
    """Ошибка валидации входных данных."""

    def __init__(self, field: str, message: str):
        # Сохраняем поле, в котором возникла ошибка
        self.field = field
        # Сохраняем сообщение об ошибке
        self.message = message
        # Передаём удобочитаемое сообщение в базовый класс
        super().__init__(f"Поле '{field}': {message}")

Использование:

try:
    raise ValidationError(field="email", message="некорректный формат")
except ValidationError as e:
    print(e.field)    # email
    print(e.message)  # некорректный формат
    print(e)          # Поле 'email': некорректный формат

Метод str для читаемого вывода

Если нужно полностью контролировать строковое представление исключения, переопределите __str__:

class BusinessRuleError(Exception):
    """Нарушение бизнес-правила."""

    def __init__(self, rule: str, details: str = ""):
        self.rule = rule
        self.details = details

    def __str__(self):
        # Формируем сообщение вручную
        base = f"Нарушено правило: {self.rule}"
        if self.details:
            return f"{base} — {self.details}"
        return base
err = BusinessRuleError(
    rule="минимальный баланс",
    details="на счету должно быть не менее 100 рублей"
)
print(err)
# Нарушено правило: минимальный баланс — на счету должно быть не менее 100 рублей

Создание иерархий исключений

В крупных приложениях удобно выстраивать дерево исключений: общий базовый класс для всего модуля, а конкретные ошибки наследуют от него. Это даёт гибкость при обработке — можно ловить и точечно, и широко.

# Базовое исключение всего приложения
class AppError(Exception):
    """Базовое исключение приложения."""
    pass


# Группа ошибок валидации
class ValidationError(AppError):
    """Ошибка валидации входных данных."""

    def __init__(self, field: str, message: str):
        self.field = field
        self.message = message
        super().__init__(f"Поле '{field}': {message}")


class RequiredFieldError(ValidationError):
    """Обязательное поле не заполнено."""

    def __init__(self, field: str):
        super().__init__(field=field, message="обязательное поле")


class InvalidFormatError(ValidationError):
    """Поле заполнено в неверном формате."""

    def __init__(self, field: str, expected_format: str):
        self.expected_format = expected_format
        super().__init__(field=field, message=f"ожидается формат: {expected_format}")


# Группа бизнес-ошибок
class BusinessError(AppError):
    """Нарушение бизнес-правила."""
    pass


class InsufficientFundsError(BusinessError):
    """Недостаточно средств на счёте."""

    def __init__(self, balance: float, required: float):
        self.balance = balance
        self.required = required
        super().__init__(
            f"Недостаточно средств: есть {balance:.2f} ₽, нужно {required:.2f} ₽"
        )


class OrderLimitExceededError(BusinessError):
    """Превышен лимит заказов."""

    def __init__(self, limit: int):
        self.limit = limit
        super().__init__(f"Превышен лимит заказов: максимум {limit} в день")

Если хотите детально освоить объектно-ориентированное программирование в Python и научиться правильно проектировать иерархии классов — приходите на наш большой курс Основы Python. На курсе 209 уроков и 34 упражнения, AI-тренажёры для практики 24/7, решение задач с живым ревью наставника, еженедельные встречи с менторами.

Ключевое слово raise — как выбрасывать исключения

Исключение выбрасывается оператором raise. Можно передать экземпляр класса или сам класс (тогда Python создаст экземпляр без аргументов).

# Выброс с явным созданием экземпляра (предпочтительный вариант)
raise ValidationError(field="username", message="не может быть пустым")

# Перевыброс пойманного исключения
try:
    do_something()
except ValueError as e:
    # Оборачиваем стандартное исключение в пользовательское
    raise ValidationError(field="age", message="должно быть числом") from e

Конструкция raise ... from e сохраняет цепочку исключений, что упрощает отладку: в трейсбеке будет видно и исходное исключение.

Использование пользовательских исключений в try/except

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

def register_user(username: str, email: str, age: int) -> dict:
    """Регистрирует нового пользователя после валидации."""
    if not username:
        raise RequiredFieldError(field="username")
    if "@" not in email:
        raise InvalidFormatError(field="email", expected_format="user@example.com")
    if age < 18:
        raise ValidationError(field="age", message="должно быть не менее 18 лет")

    return {"username": username, "email": email, "age": age}


# Точечная обработка конкретных ошибок
try:
    user = register_user(username="", email="test@test.com", age=20)
except RequiredFieldError as e:
    print(f"Не заполнено обязательное поле: {e.field}")
except InvalidFormatError as e:
    print(f"Неверный формат поля {e.field}, ожидается: {e.expected_format}")
except ValidationError as e:
    # Поймает любую ValidationError, которую не поймали выше
    print(f"Ошибка валидации: {e}")

Благодаря иерархии можно также ловить весь класс ошибок:

try:
    process_payment(user_id=1, amount=500.0)
except BusinessError as e:
    # Поймает InsufficientFundsError, OrderLimitExceededError и любой другой BusinessError
    print(f"Бизнес-ошибка: {e}")
except AppError as e:
    # Поймает всё остальное из нашего приложения
    print(f"Ошибка приложения: {e}")

Реальный пример: валидация и бизнес-логика

Рассмотрим небольшой модуль оформления заказа, где пользовательские исключения используются на всех уровнях:

class OrderService:
    """Сервис для оформления заказов."""

    MAX_DAILY_ORDERS = 5

    def __init__(self, user_balance: float, orders_today: int):
        self.user_balance = user_balance
        self.orders_today = orders_today

    def _validate_order(self, product_name: str, quantity: int, price: float) -> None:
        """Проверяет корректность данных заказа."""
        if not product_name:
            raise RequiredFieldError(field="product_name")
        if quantity <= 0:
            raise ValidationError(
                field="quantity",
                message="количество должно быть положительным числом"
            )
        if price <= 0:
            raise ValidationError(
                field="price",
                message="цена должна быть больше нуля"
            )

    def _check_business_rules(self, total: float) -> None:
        """Проверяет бизнес-правила перед оформлением заказа."""
        if self.user_balance < total:
            raise InsufficientFundsError(
                balance=self.user_balance,
                required=total
            )
        if self.orders_today >= self.MAX_DAILY_ORDERS:
            raise OrderLimitExceededError(limit=self.MAX_DAILY_ORDERS)

    def place_order(self, product_name: str, quantity: int, price: float) -> dict:
        """Оформляет заказ. Выбрасывает исключения при нарушении правил."""
        # Шаг 1: проверяем входные данные
        self._validate_order(product_name, quantity, price)

        total = quantity * price

        # Шаг 2: проверяем бизнес-правила
        self._check_business_rules(total)

        # Шаг 3: оформляем заказ
        self.user_balance -= total
        self.orders_today += 1
        return {
            "product": product_name,
            "quantity": quantity,
            "total": total,
            "remaining_balance": self.user_balance,
        }


# Использование сервиса
service = OrderService(user_balance=300.0, orders_today=2)

try:
    order = service.place_order(product_name="Ноутбук", quantity=1, price=250.0)
    print(f"Заказ оформлен: {order}")
except ValidationError as e:
    print(f"Некорректные данные — {e}")
except InsufficientFundsError as e:
    print(f"Оплата невозможна: баланс {e.balance:.2f} ₽, нужно {e.required:.2f} ₽")
except OrderLimitExceededError as e:
    print(f"Лимит заказов исчерпан: максимум {e.limit} в день")
except AppError as e:
    print(f"Непредвиденная ошибка приложения: {e}")

Соглашения об именовании

Python-сообщество придерживается нескольких устоявшихся правил именования исключений:

  • Суффикс Error — для классов, обозначающих ошибки: ValidationError, NotFoundError, AuthError.
  • Суффикс Exception — реже, как правило в библиотеках: RequestException, TimeoutException.
  • PascalCase — все классы исключений пишутся в стиле PascalCase.
  • Говорящее имя — название должно однозначно описывать ситуацию: InsufficientFundsError лучше, чем PaymentError.
  • Базовый класс модуля — в каждом пакете принято иметь один корневой <Module>Error, от которого наследуются все остальные ошибки модуля.

Частые ошибки

  • Наследование от BaseException — это нарушает стандартное поведение except Exception и мешает корректному завершению программы.
  • Слишком общие исключения — класс Error без уточнения не несёт смысла; конкретизируйте имя.
  • Забытый вызов super().__init__() — без него атрибут args будет пустым, и сообщение не выведется в трейсбеке.
  • Исключение без сообщенияraise AppError() без аргументов сложно диагностировать; всегда передавайте понятное сообщение.
  • Перехват слишком широкоexcept Exception скрывает неожиданные ошибки; ловите конкретные классы там, где это возможно.
  • Игнорирование цепочки — если оборачиваете чужое исключение в своё, используйте raise MyError(...) from original_error, чтобы не потерять контекст.

Частозадаваемые вопросы

Можно ли наследоваться от встроенных исключений, например от ValueError? Да, это полезно, когда ваша ошибка концептуально является разновидностью стандартной. Например, InvalidAgeError(ValueError) сигнализирует, что значение некорректно, и код, ловящий ValueError, тоже его поймает.

Нужно ли всегда создавать базовый класс для модуля? Не обязательно в небольших скриптах, но в библиотеках и пакетах это хорошая практика: пользователи смогут поймать все ошибки вашего модуля одним except YourLibraryError.

Как прикрепить к исключению код ошибки (например, для API)? Добавьте атрибут code в __init__:

class ApiError(Exception):
    def __init__(self, code: int, message: str):
        self.code = code
        super().__init__(message)

Можно ли использовать датаклассы для исключений? Нет: @dataclass не совместим с иерархией исключений в Python — датаклассы не вызывают super().__init__() автоматически. Определяйте __init__ вручную.

В чём разница между raise и raise ... from? raise MyError() выбрасывает новое исключение. raise MyError() from original выбрасывает его с явной цепочкой: Python покажет в трейсбеке оба исключения и отметит, что второе вызвано первым. raise MyError() from None скрывает исходное исключение.

Заключение

Пользовательские исключения — это инструмент выразительного и надёжного кода. Они позволяют точно описывать ошибки предметной области, передавать контекст и строить гибкие иерархии для удобной обработки. Ключевые принципы: наследоваться от Exception, вызывать super().__init__(), давать исключениям говорящие имена с суффиксом Error и всегда передавать достаточный контекст.

Для закрепления навыков работы с исключениями и всеми основами Python рекомендуем курс Основы Python. В первых 3 модулях курса доступно бесплатное содержание, что позволяет познакомиться с подходом и стилем преподавания, разобрать базовые концепции языка и понять структуру курса до покупки полного доступа.

Стрелочка влевоПравила именования переменных в PythonОсновы Python coreСтрелочка вправо

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonУдаление данных в Python с помощью removeОбработка исключений с помощью try/except в PythonФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonКак скачать Python на компьютерРабота с классами в PythonПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonИспользование Anaconda с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonВложенные функции в PythonСоздание собственных декораторов в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОсновные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСтруктурирование кода в PythonСоздание исполняемого файла Python в exeРазбор traceback в модуле PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота со списком значений в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиИспользование range в Python для цикловПроверка на четность в PythonПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в Python
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий