Custom Exceptions

Exception handling is an integral part of robust software development. Custom exceptions provide a way to create meaningful and descriptive error messages that make debugging easier and code more readable. This tutorial will guide you through creating and using custom exceptions in Python.

### Creating Custom Exceptions

Creating a custom exception in Python involves extending the base `Exception` class. Here's a simple example:

class CustomError(Exception):
    pass

# Raising the custom exception
def example_function(x):
    if x < 0:
        raise CustomError("CustomError: Negative value not allowed")

try:
    example_function(-5)
except CustomError as e:
    print(e)
CustomError: Negative value not allowed
- **`class CustomError(Exception)`**: Defines a new custom exception type by extending the base `Exception` class.
- **`raise CustomError("CustomError: Negative value not allowed")`**: Raises the custom exception with a custom error message.
- **`try...except`**: Catches and handles the custom exception.

### Adding Custom Attributes and Methods

Custom exceptions can have additional attributes and methods to provide more context about the error.

class ValueTooSmallError(Exception):
    def __init__(self, message, value):
        super().__init__(message)
        self.value = value

def example_function(x):
    if x < 0:
        raise ValueTooSmallError(f"ValueTooSmallError: {x} is less than 0", x)

try:
    example_function(-5)
except ValueTooSmallError as e:
    print(f"{e}: The value is {e.value}")
ValueTooSmallError: -5 is less than 0: The value is -5
- **`__init__`**: Initializes the custom exception with an additional attribute `value`.
- **`ValueTooSmallError`**: Custom error message includes the invalid value.

### Nested Custom Exceptions

You can create a hierarchy of custom exceptions to represent various error types within your application.

class MyBaseError(Exception):
    """Base class for exceptions in this module."""
    pass

class NetworkError(MyBaseError):
    """Exception raised for network-related errors."""
    def __init__(self, message="Network error occurred"):
        self.message = message
        super().__init__(self.message)

class DatabaseError(MyBaseError):
    """Exception raised for database-related errors."""
    def __init__(self, message="Database error occurred"):
        self.message = message
        super().__init__(self.message)

# Raising different custom exceptions
def network_operation():
    raise NetworkError()

def database_operation():
    raise DatabaseError()

try:
    network_operation()
except NetworkError as e:
    print(e)
except DatabaseError as e:
    print(e)
Network error occurred
- **`MyBaseError`**: Base class for all exceptions in this module.
- **`NetworkError` and `DatabaseError`**: Derived classes for specific error types.

### Using Custom Exceptions Effectively

Custom exceptions can significantly improve code readability and debugging. Here's a more comprehensive example demonstrating how to use custom exceptions in a practical scenario.

class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        super().__init__(f"Insufficient funds: Cannot withdraw {amount}, balance is {balance}")
        self.balance = balance
        self.amount = amount

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(self.balance, amount)
        self.balance -= amount
        return self.balance

# Testing the custom exception in a banking scenario
account = BankAccount(100)

try:
    account.withdraw(150)
except InsufficientFundsError as e:
    print(e)
Insufficient funds: Cannot withdraw 150, balance is 100
- **`InsufficientFundsError`**: Custom exception class for insufficient funds.
- **`withdraw()`**: Method that raises an exception if there are insufficient funds.

### Conclusion

Creating and using custom exceptions in Python allows for more descriptive and meaningful error handling, making your code easier to understand and debug. By extending the base `Exception` class, adding custom attributes, and organizing exceptions hierarchically, you can effectively manage errors in your applications.