xbe.at

Python / Testing and best practices

How to Create Custom Exception in Python

5 min read, 14 sections

Contents
  1. Understanding Custom Exceptions
  2. Creating a Domain-Specific Exception
  3. Implementing Weight Calculator with Custom Exception
  4. Hierarchical Exception Structure
  5. Advanced Exception Attributes
  6. Exception Context Management
  7. Real-world Example - Data Processing Pipeline
  8. Exception Chaining
  9. Custom Exception with Error Codes
  10. Custom Exception with Logging Integration
  11. Real-world Example - API Request Handler
  12. Exception Handler Decorator
  13. Enhanced Exception Traceback
  14. Additional Resources

Understanding Custom Exceptions

Custom exceptions in Python extend the built-in Exception class to create specialized error handling mechanisms. These allow developers to define application-specific error conditions and provide meaningful error messages tailored to their program's requirements.

# Basic structure of a custom exception
class CustomError(Exception):
    def __init__(self, message="A custom error occurred"):
        self.message = message
        super().__init__(self.message)

Creating a Domain-Specific Exception

A well-designed custom exception should encapsulate domain-specific error conditions and relevant error data. This enables precise error handling and debugging by capturing contextual information about the error state.

class InvalidWeightError(Exception):
    def __init__(self, weight, message=None):
        self.weight = weight
        self.message = message or f"Invalid weight value: {weight}"
        super().__init__(self.message)

    def __str__(self):
        return f"Weight Error: {self.message}"

Implementing Weight Calculator with Custom Exception

The weight calculator demonstrates practical usage of custom exceptions by validating input parameters and raising appropriate errors when conditions are not met. This ensures robust error handling in real-world applications.

def calculate_moon_weight(earth_weight):
    try:
        if not isinstance(earth_weight, (int, float)):
            raise InvalidWeightError(earth_weight, "Weight must be a number")
        if earth_weight < 0 or earth_weight > 300:
            raise InvalidWeightError(earth_weight, "Weight must be between 0 and 300 kg")
        
        return earth_weight * 0.165
    except InvalidWeightError as e:
        print(f"Error: {e}")
        return None

Hierarchical Exception Structure

Custom exceptions can form a hierarchy to represent different categories of errors while maintaining a common base. This approach enables more granular error handling and improved code organization.

class WeightError(Exception):
    """Base exception for weight-related errors"""
    pass

class NegativeWeightError(WeightError):
    def __init__(self, weight):
        super().__init__(f"Weight cannot be negative: {weight}")

class ExcessiveWeightError(WeightError):
    def __init__(self, weight, limit):
        super().__init__(f"Weight {weight} exceeds limit of {limit}")

Advanced Exception Attributes

Complex applications often require exceptions to carry additional data for debugging and logging purposes. Custom exceptions can include specialized attributes and methods to enhance error reporting.

class DataValidationError(Exception):
    def __init__(self, value, expected_type, constraints=None):
        self.value = value
        self.expected_type = expected_type
        self.constraints = constraints or {}
        self.timestamp = datetime.now()
        message = self._build_message()
        super().__init__(message)

    def _build_message(self):
        return f"Validation failed for value {self.value} (type: {type(self.value)})"

Exception Context Management

Custom exceptions can be integrated with context managers to ensure proper resource handling and cleanup, even when errors occur during execution.

class DatabaseConnection:
    class ConnectionError(Exception):
        def __init__(self, operation, details):
            self.operation = operation
            self.details = details
            super().__init__(f"Database {operation} failed: {details}")

    def __enter__(self):
        if not self.connect():
            raise self.ConnectionError("connect", "Unable to establish connection")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.disconnect()

Real-world Example - Data Processing Pipeline

This example demonstrates a practical implementation of custom exceptions in a data processing pipeline, handling various error conditions that might occur during data transformation.

class DataProcessingError(Exception):
    def __init__(self, stage, error_type, details):
        self.stage = stage
        self.error_type = error_type
        self.details = details
        super().__init__(f"Error in {stage}: {error_type} - {details}")

def process_dataset(data):
    try:
        if not isinstance(data, list):
            raise DataProcessingError("validation", "TypeError", "Input must be a list")
        
        processed = []
        for idx, item in enumerate(data):
            if not item.strip():
                raise DataProcessingError("processing", "ValueError", 
                                       f"Empty value at index {idx}")
            processed.append(item.upper())
        return processed
    except DataProcessingError as e:
        print(f"Processing failed: {e}")
        return None

Exception Chaining

Exception chaining allows preservation of the original error while raising a new, more specific exception. This maintains the full error context for debugging purposes.

class FileProcessingError(Exception):
    pass

def process_config_file(filename):
    try:
        with open(filename) as f:
            config = json.load(f)
    except FileNotFoundError as e:
        raise FileProcessingError(
            f"Configuration file {filename} not found"
        ) from e
    except json.JSONDecodeError as e:
        raise FileProcessingError(
            f"Invalid JSON in {filename}"
        ) from e

Custom Exception with Error Codes

Custom exceptions can incorporate error codes to provide standardized error handling across an application. This approach facilitates automated error processing and internationalization of error messages.

class SystemError(Exception):
    def __init__(self, code, message):
        self.code = code
        self.message = message
        super().__init__(f"Error {code}: {message}")

    @classmethod
    def resource_not_found(cls, resource_id):
        return cls("E404", f"Resource {resource_id} not found")

    @classmethod
    def permission_denied(cls, operation):
        return cls("E403", f"Permission denied for operation: {operation}")

Custom Exception with Logging Integration

Integrating logging capabilities into custom exceptions enables automatic tracking of error occurrences and simplified debugging in production environments.

import logging
from datetime import datetime

class LoggedError(Exception):
    def __init__(self, message, logger=None):
        self.timestamp = datetime.now()
        self.logger = logger or logging.getLogger(__name__)
        
        super().__init__(message)
        self._log_error()
    
    def _log_error(self):
        error_info = {
            'message': str(self),
            'timestamp': self.timestamp,
            'type': self.__class__.__name__
        }
        self.logger.error(f"Error occurred: {error_info}")

Real-world Example - API Request Handler

This implementation shows how custom exceptions can be used in an API request handler to manage different types of request failures and provide appropriate responses.

class APIError(Exception):
    def __init__(self, status_code, message, details=None):
        self.status_code = status_code
        self.message = message
        self.details = details or {}
        super().__init__(message)

class RequestHandler:
    def process_request(self, request_data):
        try:
            if not request_data:
                raise APIError(400, "Empty request body")
            
            if 'auth_token' not in request_data:
                raise APIError(401, "Missing authentication token")
            
            if not self.validate_token(request_data['auth_token']):
                raise APIError(403, "Invalid authentication token")
                
            return self.handle_validated_request(request_data)
            
        except APIError as e:
            return {
                'status': 'error',
                'code': e.status_code,
                'message': e.message,
                'details': e.details
            }

Exception Handler Decorator

Creating a decorator for exception handling provides a clean way to implement consistent error handling across multiple functions while maintaining code readability.

from functools import wraps

def handle_exceptions(error_map=None):
    error_map = error_map or {}
    
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                error_type = type(e)
                if error_type in error_map:
                    raise error_map[error_type](str(e))
                raise
        return wrapper
    return decorator

@handle_exceptions({
    ValueError: CustomError,
    KeyError: DataValidationError
})
def process_data(data):
    # Function implementation
    pass

Enhanced Exception Traceback

Custom exceptions can be enhanced with detailed traceback information to provide comprehensive debugging capabilities in complex applications.

import traceback
import sys

class DetailedError(Exception):
    def __init__(self, message, **context):
        super().__init__(message)
        self.context = context
        self.traceback = self._capture_traceback()
    
    def _capture_traceback(self):
        exc_type, exc_value, exc_traceback = sys.exc_info()
        if exc_traceback:
            return ''.join(traceback.format_tb(exc_traceback))
        return ''.join(traceback.format_stack()[:-1])
    
    def get_error_details(self):
        return {
            'message': str(self),
            'context': self.context,
            'traceback': self.traceback
        }

Additional Resources

More in Testing and best practices

  1. Logging Configuration and Handlers in Python13 min

    The logging module in Python provides a flexible framework for generating log messages with different severity levels. Understanding the basic configuration...

  2. Exploring Python Docstring Formats15 min

    The reStructuredText (reST) docstring format, originally developed for Python documentation, uses a semantic markup syntax to define documentation elements....

  3. Mastering Python Error Handling3 min

    Handling Basics Exception handling in Python is a way to deal with runtime errors gracefully. When an error occurs, instead of crashing, your program can...

  4. Exception Handling Patterns in Python12 min

    Exception handling in Python provides a robust mechanism for dealing with runtime errors through try-except blocks. This fundamental pattern allows...

  5. Mastering Python Error Handling with Try Except Else and Finally7 min

    Exception handling is a crucial aspect of writing robust Python code. It allows developers to gracefully manage errors and unexpected situations that may...

  6. Exception Handling in Python A Comprehensive Guide8 min

    Exceptions are events that occur during program execution which disrupt the normal flow of instructions. In Python, exceptions are objects that represent...