Ojasa Mirai

Ojasa Mirai

Python

Loading...

Learning Level

🟢 Beginner🔵 Advanced
Exceptions OverviewException TypesTry-Except BlocksRaising ExceptionsCustom ExceptionsMultiple ExceptionsFinally & CleanupDebugging TechniquesLogging Best Practices
Python/Error Handling/Raising Exceptions

🎯 Raising Exceptions — Advanced Patterns and Design Principles

Master sophisticated exception raising patterns for enterprise systems.


📋 Design by Contract

Implement contracts using exceptions:

# Design by contract: preconditions, postconditions, invariants
class BankAccount:
    def __init__(self, balance=0):
        self._balance = balance
        self._check_invariant()

    def _check_invariant(self):
        """Check that object is in valid state"""
        if self._balance < 0:
            raise InvariantViolationError("Balance cannot be negative")

    def withdraw(self, amount):
        """
        Precondition: amount > 0 and amount <= balance
        Postcondition: new_balance = old_balance - amount
        """
        # Check preconditions
        if not isinstance(amount, (int, float)):
            raise PreconditionError("Amount must be numeric")
        if amount <= 0:
            raise PreconditionError("Amount must be positive")
        if amount > self._balance:
            raise PreconditionError(f"Insufficient funds: need {amount}, have {self._balance}")

        old_balance = self._balance
        self._balance -= amount

        # Check postcondition
        if self._balance != old_balance - amount:
            raise PostconditionError("Balance calculation error")

        # Check invariant
        self._check_invariant()
        return self._balance

class ContractError(Exception):
    pass

class PreconditionError(ContractError):
    pass

class PostconditionError(ContractError):
    pass

class InvariantViolationError(ContractError):
    pass

# Usage
account = BankAccount(100)
try:
    account.withdraw(150)  # Precondition violation
except PreconditionError as e:
    print(f"Contract violation: {e}")

🔍 Sophisticated Validation

# Multi-level validation strategy
class ValidationError(Exception):
    def __init__(self, field, constraint, value):
        self.field = field
        self.constraint = constraint
        self.value = value
        super().__init__(f"{field}: {constraint}")

class ValidationRules:
    """Composable validation rules"""
    def __init__(self):
        self.rules = []

    def add_rule(self, predicate, message):
        self.rules.append((predicate, message))
        return self

    def validate(self, value):
        for predicate, message in self.rules:
            if not predicate(value):
                raise ValidationError('field', message, value)

# Usage
email_rules = ValidationRules()\
    .add_rule(lambda x: '@' in x, "must contain @")\
    .add_rule(lambda x: '.' in x.split('@')[1], "domain must have extension")\
    .add_rule(lambda x: len(x) > 5, "must be at least 5 characters")

def validate_email(email):
    email_rules.validate(email)
    return True

try:
    validate_email("invalid")
except ValidationError as e:
    print(f"Validation failed: {e}")

# Complex nested validation
class User:
    def __init__(self, name, email, age):
        self._validate_and_set('name', name, str, lambda x: len(x) > 0)
        self._validate_and_set('email', email, str, self._is_valid_email)
        self._validate_and_set('age', age, int, lambda x: 0 < x < 150)

    def _validate_and_set(self, field, value, expected_type, predicate):
        if not isinstance(value, expected_type):
            raise ValidationError(field, f"must be {expected_type.__name__}", value)
        if not predicate(value):
            raise ValidationError(field, "constraint failed", value)
        setattr(self, f'_{field}', value)

    @staticmethod
    def _is_valid_email(email):
        return '@' in email and '.' in email.split('@')[1]

# Usage
try:
    user = User("Alice", "alice@example.com", 25)
except ValidationError as e:
    print(f"User creation failed: {e}")

🏗️ Exception Raising Strategies for Complex Systems

# Strategy 1: Guard clauses
def process_order(order):
    """Use early returns to fail fast"""
    if not order:
        raise ValueError("Order cannot be None")
    if order['items'] is None or len(order['items']) == 0:
        raise ValueError("Order must have at least one item")
    if order['total'] <= 0:
        raise ValueError("Order total must be positive")
    if order['customer_id'] is None:
        raise ValueError("Order must have customer")

    # Only process if all conditions met
    return _process_valid_order(order)

# Strategy 2: Accumulate errors
def validate_form(form_data):
    """Collect all errors before raising"""
    errors = {}

    if not form_data.get('username'):
        errors['username'] = "Required"
    elif len(form_data['username']) < 3:
        errors['username'] = "Must be at least 3 characters"

    if not form_data.get('password'):
        errors['password'] = "Required"
    elif len(form_data['password']) < 8:
        errors['password'] = "Must be at least 8 characters"

    if not form_data.get('email'):
        errors['email'] = "Required"
    elif '@' not in form_data['email']:
        errors['email'] = "Invalid email format"

    if errors:
        raise FormValidationError(errors)

    return True

class FormValidationError(Exception):
    def __init__(self, errors):
        self.errors = errors
        super().__init__(f"Form validation failed with {len(errors)} errors")

# Strategy 3: Conditional raising
def safe_convert_to_int(value, allow_none=False):
    """Raise only if truly invalid"""
    if value is None:
        if allow_none:
            return None
        raise ValueError("Value cannot be None")

    if isinstance(value, int):
        return value

    if isinstance(value, str):
        if value.isdigit():
            return int(value)
        raise ValueError(f"'{value}' is not a valid integer")

    raise TypeError(f"Cannot convert {type(value).__name__} to int")

🎨 Contextual Exception Information

# Include rich context in exceptions
class ContextualException(Exception):
    def __init__(self, message, context=None):
        self.message = message
        self.context = context or {}
        self.timestamp = datetime.now()
        super().__init__(self._format_message())

    def _format_message(self):
        msg = self.message
        if self.context:
            ctx_str = ', '.join(f"{k}={v}" for k, v in self.context.items())
            msg = f"{msg} | Context: {ctx_str}"
        return msg

    def add_context(self, **kwargs):
        self.context.update(kwargs)
        return self

# Usage
def api_request(endpoint, method='GET', timeout=5, retries=3):
    for attempt in range(retries):
        try:
            return make_request(endpoint, method=method, timeout=timeout)
        except requests.RequestException as e:
            if attempt == retries - 1:
                raise ContextualException(
                    "API request failed",
                    context={
                        'endpoint': endpoint,
                        'method': method,
                        'attempts': attempt + 1,
                        'error': str(e),
                        'timestamp': datetime.now()
                    }
                ) from e

# Exception with structured data
class StructuredError(Exception):
    """Exception with JSON-serializable structure"""
    def __init__(self, error_code, message, details=None):
        self.error_code = error_code
        self.message = message
        self.details = details or {}
        super().__init__(message)

    def to_json(self):
        return {
            'error_code': self.error_code,
            'message': self.message,
            'details': self.details,
            'timestamp': datetime.now().isoformat()
        }

def process_payment(amount, card_token):
    try:
        charge_card(card_token, amount)
    except CardDeclined as e:
        raise StructuredError(
            error_code='PAYMENT_DECLINED',
            message='Payment was declined',
            details={
                'reason': str(e),
                'amount': amount,
                'card_last_4': card_token[-4:]
            }
        ) from e

🔄 Conditional and State-Dependent Raising

# Raise based on system state
class StatefulOperation:
    def __init__(self):
        self.state = 'IDLE'
        self.data = None

    def start(self):
        if self.state != 'IDLE':
            raise InvalidStateError(
                f"Cannot start from {self.state} state. Must be IDLE"
            )
        self.state = 'RUNNING'

    def process(self, data):
        if self.state != 'RUNNING':
            raise InvalidStateError(
                f"Cannot process in {self.state} state. Must be RUNNING"
            )
        self.data = data
        self.state = 'PROCESSING'

    def finish(self):
        if self.state not in ('RUNNING', 'PROCESSING'):
            raise InvalidStateError(
                f"Cannot finish from {self.state} state"
            )
        self.state = 'COMPLETE'

class InvalidStateError(Exception):
    pass

# Raise with different messages based on condition
def create_resource(resource_type, name, quota):
    """Raise different errors based on why creation failed"""
    if resource_type not in ['compute', 'storage', 'network']:
        raise InvalidResourceType(f"Unknown resource type: {resource_type}")

    if not name or len(name) < 3:
        raise InvalidResourceName("Name must be at least 3 characters")

    if quota.get(resource_type, 0) <= 0:
        raise QuotaExceeded(
            f"No quota available for {resource_type}. "
            f"Limit: {quota.get(f'{resource_type}_limit', 0)}"
        )

    return _create_resource(resource_type, name, quota)

⚡ Performance Considerations

# Avoid creating expensive exceptions
class ExpensiveException(Exception):
    def __init__(self, data):
        # Don't serialize large data in exception
        self.data = data
        super().__init__(f"Error processing {len(data)} items")

# Better: store only summary
class EfficientException(Exception):
    def __init__(self, data_size, error_message):
        self.data_size = data_size
        self.error_message = error_message
        super().__init__(error_message)

# Pre-check conditions to avoid exception creation
def validate_large_dataset(dataset):
    # Check once before processing
    if len(dataset) == 0:
        raise EmptyDatasetError()
    if len(dataset) > 1000000:
        raise DatasetTooLargeError()

    # Process without raising exceptions in loop
    results = []
    for item in dataset:
        try:
            results.append(process_item(item))
        except ProcessingError:
            continue  # Skip bad items instead of accumulating exceptions

    if not results:
        raise NoValidItemsError()

    return results

🎯 Key Takeaways

  • ✅ Design by contract using preconditions, postconditions, invariants
  • ✅ Create composable validation rules
  • ✅ Use guard clauses for early failure
  • ✅ Accumulate errors for better user feedback
  • ✅ Include rich context in exceptions
  • ✅ Raise conditionally based on system state
  • ✅ Consider performance when raising exceptions


Resources

Python Docs

Ojasa Mirai

Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.

Learn Deep • Build Real • Verify Skills • Launch Forward

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn