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/Custom Exceptions

🎨 Custom Exceptions — Creating Domain-Specific Errors

Learn how to create your own exception classes that make your code more expressive and easier to debug.


📦 Creating Simple Custom Exceptions

A custom exception is just a class that inherits from `Exception`:

# Simplest custom exception
class InsufficientFundsError(Exception):
    pass

class InvalidAccountError(Exception):
    pass

# Use them like built-in exceptions
def withdraw(balance, amount):
    if not isinstance(amount, (int, float)):
        raise TypeError("Amount must be a number")

    if amount < 0:
        raise ValueError("Amount cannot be negative")

    if amount > balance:
        raise InsufficientFundsError(f"Cannot withdraw ${amount}. Balance: ${balance}")

    return balance - amount

# Test it
account_balance = 100.00

try:
    new_balance = withdraw(account_balance, 50)
    print(f"Withdrawal successful. New balance: ${new_balance}")
except InsufficientFundsError as e:
    print(f"Transaction failed: {e}")

🔍 Custom Exceptions with Additional Data

You can store extra information in custom exceptions:

# Custom exception with details
class TransactionError(Exception):
    def __init__(self, message, transaction_id=None, amount=None):
        super().__init__(message)
        self.transaction_id = transaction_id
        self.amount = amount

class InsufficientFundsError(TransactionError):
    pass

# Use it with extra data
def process_payment(account_id, amount, balance):
    if amount > balance:
        raise InsufficientFundsError(
            f"Cannot charge ${amount}",
            transaction_id=f"TXN_{account_id}",
            amount=amount
        )
    return balance - amount

# Catch and access the data
try:
    new_balance = process_payment("ACC123", 150, 100)
except InsufficientFundsError as e:
    print(f"Error: {e}")
    print(f"Transaction ID: {e.transaction_id}")
    print(f"Amount: ${e.amount}")

🏛️ Exception Hierarchy with Custom Exceptions

Create your own exception hierarchy:

# Base custom exception
class APIError(Exception):
    """Base exception for API-related errors"""
    pass

# Specific API errors
class APIConnectionError(APIError):
    """Connection to API failed"""
    pass

class APITimeoutError(APIError):
    """API request timed out"""
    pass

class APIResponseError(APIError):
    """Received invalid response from API"""
    pass

class APIAuthenticationError(APIError):
    """Authentication with API failed"""
    pass

# Using the hierarchy
def fetch_user_data(user_id, api_key):
    try:
        # Simulate API call
        if not api_key:
            raise APIAuthenticationError("Missing API key")

        if user_id < 0:
            raise ValueError("User ID must be positive")

        # Simulate successful response
        return {"id": user_id, "name": f"User {user_id}"}

    except APIAuthenticationError:
        print("Please provide valid API credentials")
        raise
    except APIConnectionError:
        print("Cannot connect to API. Check your connection.")
        raise
    except APIError as e:
        print(f"API error occurred: {e}")
        raise

# Catching different levels
try:
    user = fetch_user_data(-1, "key123")
except APIAuthenticationError:
    print("Authentication failed - please login again")
except APIError:
    print("API error - please try again later")

📝 Documentation in Custom Exceptions

# Well-documented custom exceptions
class ValidationError(Exception):
    """
    Raised when user input validation fails.

    Attributes:
        field (str): The field that failed validation
        value: The invalid value
        rule (str): The validation rule that was violated
    """
    def __init__(self, field, value, rule):
        self.field = field
        self.value = value
        self.rule = rule
        message = f"{field} validation failed: {rule} (got: {value})"
        super().__init__(message)

class AgeValidationError(ValidationError):
    """Raised when age validation fails."""
    pass

class EmailValidationError(ValidationError):
    """Raised when email validation fails."""
    pass

# Usage
def validate_age(age):
    if not isinstance(age, int):
        raise AgeValidationError("age", age, "must be an integer")
    if age < 0:
        raise AgeValidationError("age", age, "cannot be negative")
    if age > 150:
        raise AgeValidationError("age", age, "must be realistic")

try:
    validate_age("25")
except AgeValidationError as e:
    print(f"Error: {e}")
    print(f"Field: {e.field}")
    print(f"Invalid value: {e.value}")
    print(f"Rule broken: {e.rule}")

🎯 Practical Custom Exception Examples

# Example 1: Game exceptions
class GameError(Exception):
    pass

class InvalidMoveError(GameError):
    def __init__(self, move, reason):
        self.move = move
        self.reason = reason
        super().__init__(f"Invalid move '{move}': {reason}")

class GameOverError(GameError):
    def __init__(self, winner):
        self.winner = winner
        super().__init__(f"Game over! Winner: {winner}")

def make_move(board, position, player):
    if position < 0 or position >= len(board):
        raise InvalidMoveError(position, "position out of bounds")

    if board[position] is not None:
        raise InvalidMoveError(position, "position already taken")

    board[position] = player
    return board

# Test
try:
    board = [None] * 9
    make_move(board, 10, "X")
except InvalidMoveError as e:
    print(f"Error: {e.reason}")

# Example 2: Database exceptions
class DatabaseError(Exception):
    pass

class ConnectionError(DatabaseError):
    """Cannot connect to database"""
    pass

class QueryError(DatabaseError):
    """Database query failed"""
    pass

class DataIntegrityError(DatabaseError):
    """Data violates integrity constraints"""
    pass

def execute_query(query, db_connection):
    if not db_connection:
        raise ConnectionError("No database connection")

    if "DROP" in query.upper():
        raise DataIntegrityError("Cannot execute destructive queries")

    return {"status": "success"}

try:
    execute_query("DROP TABLE users", None)
except DatabaseError as e:
    print(f"Database error: {e}")

# Example 3: Configuration exceptions
class ConfigurationError(Exception):
    """Base configuration error"""
    pass

class ConfigNotFoundError(ConfigurationError):
    def __init__(self, key):
        self.key = key
        super().__init__(f"Configuration key '{key}' not found")

class ConfigValueError(ConfigurationError):
    def __init__(self, key, expected_type):
        self.key = key
        self.expected_type = expected_type
        super().__init__(f"Config '{key}' must be {expected_type}")

def get_config(config_dict, key, expected_type):
    if key not in config_dict:
        raise ConfigNotFoundError(key)

    value = config_dict[key]
    if not isinstance(value, expected_type):
        raise ConfigValueError(key, expected_type.__name__)

    return value

try:
    config = {"debug": "yes"}  # Should be boolean
    debug_mode = get_config(config, "debug", bool)
except ConfigValueError as e:
    print(f"Config error: {e}")

🎯 Best Practices for Custom Exceptions

# ✅ Good: Specific, descriptive, inherited from Exception
class FileProcessingError(Exception):
    """Base exception for file processing"""
    pass

class FileNotFoundError(FileProcessingError):
    """File doesn't exist"""
    pass

class FilePermissionError(FileProcessingError):
    """Don't have permission to access file"""
    pass

# ❌ Avoid: Inheriting from wrong class
# class MyException(ValueError):  # Confusing!
#     pass

# ✅ Good: Meaningful names that describe the error
class InvalidCredentialsError(Exception):
    pass

# ❌ Avoid: Generic or unclear names
# class Error(Exception):
#     pass

# ✅ Good: Provide context in error messages
def process_file(filename):
    try:
        with open(filename) as f:
            return f.read()
    except FileNotFoundError:
        raise FileProcessingError(f"Cannot find {filename} in current directory")

# ❌ Avoid: Confusing error messages
# raise FileProcessingError("error")

🎯 Key Takeaways

  • ✅ Create custom exceptions by inheriting from `Exception`
  • ✅ Name them to describe what went wrong
  • ✅ Can store additional data in custom exceptions
  • ✅ Create exception hierarchies for related errors
  • ✅ Helps make code more maintainable and understandable
  • ✅ Use in domain-specific applications (games, APIs, etc.)


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