# 14. Custom Exceptions

These examples show how to design custom exception classes for specific error-handling scenarios, allowing for more granular and meaningful error reporting in your Python applications.

***

**1. Basic Custom Exception**

```python
class MyCustomError(Exception):
    pass

def divide(a, b):
    if b == 0:
        raise MyCustomError("Cannot divide by zero!")
    return a / b

try:
    result = divide(10, 0)
except MyCustomError as e:
    print(f"Error: {e}")
```

***

**2. Custom Exception with Custom Message**

```python
class InvalidAgeError(Exception):
    def __init__(self, age, message="Age must be greater than 0"):
        self.age = age
        self.message = message
        super().__init__(self.message)

def validate_age(age):
    if age <= 0:
        raise InvalidAgeError(age)
    print(f"Age {age} is valid.")

try:
    validate_age(0)
except InvalidAgeError as e:
    print(f"Error: {e}")
```

***

**3. Custom Exception with Error Code**

```python
class InvalidValueError(Exception):
    def __init__(self, value, code, message="Invalid value provided"):
        self.value = value
        self.code = code
        self.message = message
        super().__init__(self.message)

    def __str__(self):
        return f"[Error Code {self.code}] {self.message}: {self.value}"

def process_value(value):
    if value < 0:
        raise InvalidValueError(value, 404)
    return value * 2

try:
    process_value(-1)
except InvalidValueError as e:
    print(f"Error: {e}")
```

***

**4. Custom Exception with Traceback**

```python
import traceback

class CustomFileError(Exception):
    pass

def read_file(file_path):
    try:
        with open(file_path, 'r') as file:
            return file.read()
    except FileNotFoundError as e:
        raise CustomFileError(f"File {file_path} not found") from e

try:
    read_file("nonexistent_file.txt")
except CustomFileError as e:
    print(f"Error: {e}")
    traceback.print_exc()
```

***

**5. Custom Exception with Multiple Error Types**

```python
class FileReadError(Exception):
    pass

class FileWriteError(Exception):
    pass

def read_file(file_path):
    try:
        with open(file_path, 'r') as file:
            return file.read()
    except FileNotFoundError:
        raise FileReadError("File not found during read operation.")
    except PermissionError:
        raise FileReadError("Permission denied during read operation.")

def write_file(file_path, content):
    try:
        with open(file_path, 'w') as file:
            file.write(content)
    except PermissionError:
        raise FileWriteError("Permission denied during write operation.")

try:
    read_file("missing_file.txt")
except FileReadError as e:
    print(f"Read error: {e}")

try:
    write_file("/restricted_file.txt", "Hello")
except FileWriteError as e:
    print(f"Write error: {e}")
```

***

**6. Custom Exception with Additional Data**

```python
class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        self.message = f"Insufficient funds: Balance {balance}, attempted withdrawal {amount}"
        super().__init__(self.message)

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

try:
    withdraw(50, 100)
except InsufficientFundsError as e:
    print(f"Error: {e}")
```

***

**7. Custom Exception with Repr Method**

```python
class NegativeNumberError(Exception):
    def __init__(self, number):
        self.number = number
        self.message = f"Negative number is not allowed: {number}"
        super().__init__(self.message)

    def __repr__(self):
        return f"NegativeNumberError({self.number!r})"

def process_number(number):
    if number < 0:
        raise NegativeNumberError(number)

try:
    process_number(-5)
except NegativeNumberError as e:
    print(repr(e))  # Output: NegativeNumberError(-5)
```

***

**8. Custom Exception for Authentication Failure**

```python
class AuthenticationError(Exception):
    def __init__(self, username):
        self.username = username
        self.message = f"Authentication failed for user: {username}"
        super().__init__(self.message)

def authenticate(user, password):
    if user != "admin" or password != "password123":
        raise AuthenticationError(user)
    print("Authenticated successfully!")

try:
    authenticate("guest", "wrongpassword")
except AuthenticationError as e:
    print(f"Error: {e}")
```

***

**9. Custom Exception for Invalid Input**

```python
class InvalidInputError(Exception):
    def __init__(self, input_value, message="Invalid input"):
        self.input_value = input_value
        self.message = message
        super().__init__(self.message)

def process_input(value):
    if not isinstance(value, int):
        raise InvalidInputError(value)
    return value * 2

try:
    process_input("string")
except InvalidInputError as e:
    print(f"Error: {e}")
```

***

**10. Chaining Exceptions**

```python
class InvalidValueError(Exception):
    pass

class InvalidOperationError(Exception):
    pass

def perform_operation(a, b):
    try:
        if a < 0 or b < 0:
            raise InvalidValueError("Negative numbers are not allowed")
        return a / b
    except InvalidValueError as e:
        raise InvalidOperationError("Operation failed due to invalid values") from e

try:
    perform_operation(-1, 10)
except InvalidOperationError as e:
    print(f"Error: {e}")
    print(f"Caused by: {e.__cause__}")
```

***
