← Назад к вопросам

Как написать своё исключение в Python?

1.7 Middle🔥 251 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Создание собственных исключений в Python

Пользовательские исключения — это один из инструментов для построения чистого и поддерживаемого кода. Они позволяют явно обработать специфичные для вашего приложения ошибочные ситуации.

1. Базовое пользовательское исключение

Самый простой способ — наследование от Exception:

class CustomError(Exception):
    pass

# Использование
try:
    raise CustomError("Что-то пошло не так")
except CustomError as e:
    print(f"Поймано исключение: {e}")

2. Исключение с собственными атрибутами

class ValidationError(Exception):
    def __init__(self, message: str, field: str, value: any):
        self.message = message
        self.field = field
        self.value = value
        super().__init__(self.message)
    
    def __str__(self):
        return f"Ошибка валидации в поле '{self.field}': {self.message}"

# Использование
try:
    raise ValidationError(
        message="Email должен содержать @",
        field="email",
        value="invalid-email"
    )
except ValidationError as e:
    print(e)
    print(f"Поле: {e.field}, Значение: {e.value}")

Вывод:

Ошибка валидации в поле 'email': Email должен содержать @
Поле: email, Значение: invalid-email

3. Иерархия исключений

Создайте базовое исключение для всего приложения:

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

# Специфичные исключения
class ValidationError(AppError):
    pass

class NotFoundError(AppError):
    pass

class AuthenticationError(AppError):
    pass

class PermissionError(AppError):
    pass

# Использование
try:
    user = find_user_by_id(999)
    if not user:
        raise NotFoundError("Пользователь не найден")
except NotFoundError as e:
    print(f"404: {e}")
except AppError as e:
    print(f"Общая ошибка приложения: {e}")

4. Исключение с дополнительным контекстом

class APIError(Exception):
    def __init__(self, message: str, status_code: int, response_body: dict = None):
        self.message = message
        self.status_code = status_code
        self.response_body = response_body or {}
        super().__init__(self.message)
    
    def to_dict(self):
        return {
            'error': self.message,
            'status': self.status_code,
            'details': self.response_body
        }

# Использование в API обработчике
def get_user_from_api(user_id: int):
    try:
        response = requests.get(f'https://api.example.com/users/{user_id}')
        if response.status_code == 404:
            raise APIError(
                message="User not found",
                status_code=404,
                response_body={'user_id': user_id}
            )
    except APIError as e:
        return {"error": e.to_dict()}

5. Исключение с кастомной функцией str

class DatabaseError(Exception):
    def __init__(self, operation: str, table: str, reason: str):
        self.operation = operation
        self.table = table
        self.reason = reason
        super().__init__()
    
    def __str__(self):
        return f"Database error: {self.operation} in table '{self.table}' - {self.reason}"
    
    def __repr__(self):
        return f"DatabaseError(operation='{self.operation}', table='{self.table}', reason='{self.reason}')"

# Использование
try:
    raise DatabaseError(
        operation='INSERT',
        table='users',
        reason='Duplicate key violation'
    )
except DatabaseError as e:
    print(str(e))  # Database error: INSERT in table 'users' - Duplicate key violation
    print(repr(e))  # DatabaseError(operation='INSERT', table='users', reason='Duplicate key violation')

6. Исключение с поддержкой цепочки ошибок

class ProcessingError(Exception):
    def __init__(self, message: str, original_error: Exception = None):
        self.message = message
        self.original_error = original_error
        super().__init__(self.message)

# Использование
def process_file(filename: str):
    try:
        with open(filename, 'r') as f:
            data = json.load(f)
    except FileNotFoundError as e:
        raise ProcessingError(
            message=f"Не удалось открыть файл {filename}",
            original_error=e
        )
    except json.JSONDecodeError as e:
        raise ProcessingError(
            message=f"Некорректный JSON в файле {filename}",
            original_error=e
        )

try:
    process_file('data.json')
except ProcessingError as e:
    print(f"Ошибка обработки: {e.message}")
    print(f"Оригинальная ошибка: {e.original_error}")

7. Современный подход: использование raise ... from ...

class ConfigError(Exception):
    pass

def load_config(config_file: str):
    try:
        with open(config_file) as f:
            return json.load(f)
    except FileNotFoundError as e:
        raise ConfigError(f"Config file not found: {config_file}") from e
    except json.JSONDecodeError as e:
        raise ConfigError(f"Invalid JSON in config: {config_file}") from e

# Это сохраняет оригинальный stack trace
try:
    load_config('missing.json')
except ConfigError as e:
    print(e)  # Config file not found: missing.json
    print(e.__cause__)  # Оригинальная FileNotFoundError

8. Исключение для бизнес-логики

class InsufficientFundsError(Exception):
    def __init__(self, required: float, available: float):
        self.required = required
        self.available = available
        super().__init__(f"Недостаточно средств: требуется {required}, доступно {available}")

class InvalidTransactionError(Exception):
    def __init__(self, reason: str, transaction_id: str):
        self.reason = reason
        self.transaction_id = transaction_id
        super().__init__(f"Invalid transaction {transaction_id}: {reason}")

# Использование
def transfer_money(from_account: int, to_account: int, amount: float):
    balance = get_balance(from_account)
    
    if balance < amount:
        raise InsufficientFundsError(required=amount, available=balance)
    
    if from_account == to_account:
        raise InvalidTransactionError(
            reason="Cannot transfer to same account",
            transaction_id=str(uuid.uuid4())
        )
    
    # Выполняем транзакцию
    process_transfer(from_account, to_account, amount)

9. Контекстный менеджер для исключений

from contextlib import contextmanager

class TimeoutError(Exception):
    pass

@contextmanager
def timeout(seconds: int):
    import signal
    
    def timeout_handler(signum, frame):
        raise TimeoutError(f"Operation timed out after {seconds} seconds")
    
    signal.signal(signal.SIGALRM, timeout_handler)
    signal.alarm(seconds)
    
    try:
        yield
    finally:
        signal.alarm(0)

# Использование
try:
    with timeout(2):
        time.sleep(3)  # Это вызовет TimeoutError
except TimeoutError as e:
    print(f"Timeout: {e}")

10. Лучшие практики

Структурируйте иерархию:

# Приложение
class MyAppError(Exception):
    pass

# Слой API
class APIError(MyAppError):
    pass

class ValidationError(APIError):
    pass

class AuthenticationError(APIError):
    pass

# Слой БД
class DatabaseError(MyAppError):
    pass

class DuplicateKeyError(DatabaseError):
    pass

# Обработка
try:
    api_call()
except ValidationError as e:
    return Response(status=400, body={"error": str(e)})
except AuthenticationError as e:
    return Response(status=401, body={"error": str(e)})
except APIError as e:
    return Response(status=500, body={"error": str(e)})

Документируйте:

class RateLimitError(Exception):
    """Выбрасывается когда превышена частота запросов.
    
    Args:
        retry_after: Количество секунд, которые нужно ждать перед повторной попыткой
    """
    def __init__(self, retry_after: int):
        self.retry_after = retry_after
        super().__init__(f"Rate limit exceeded. Retry after {retry_after} seconds")

Итог

  • Наследуйте от Exception для пользовательских исключений
  • Создавайте иерархию для логичной обработки
  • Добавляйте контекст (поля, методы) для отладки
  • Используйте raise ... from ... для сохранения оригинального traceback
  • Документируйте исключения в docstring функций