Gyaan

Custom Exceptions

intermediate exceptions custom-exceptions error-classes

Python’s built-in exceptions like ValueError and TypeError are great, but sometimes we need errors that are specific to our application. That’s where custom exceptions come in.

Why Create Custom Exceptions?

Imagine we’re building a payment system. When a payment fails, raising a generic ValueError doesn’t tell us much. But a PaymentFailedError with the transaction ID? Now we’re talking.

Custom exceptions let us:

  • Give meaningful names to errors in our domain
  • Attach extra data (like error codes or context)
  • Let callers catch our specific errors without catching unrelated ones

The Basics

A custom exception is just a class that inherits from Exception. That’s it.

class PaymentFailedError(Exception):
    pass

# Using it
raise PaymentFailedError("Insufficient funds")

Important: Always inherit from Exception, not BaseException. The BaseException class includes things like KeyboardInterrupt and SystemExit — we don’t want to accidentally catch those.

Adding Custom Attributes

The real power comes when we attach extra information to our exceptions.

class PaymentFailedError(Exception):
    def __init__(self, message, transaction_id=None, error_code=None):
        super().__init__(message)
        self.transaction_id = transaction_id
        self.error_code = error_code

# Now we can catch it and access the details
try:
    raise PaymentFailedError(
        "Card declined",
        transaction_id="txn_abc123",
        error_code=402
    )
except PaymentFailedError as e:
    print(f"Payment error: {e}")                    # Card declined
    print(f"Transaction: {e.transaction_id}")       # txn_abc123
    print(f"Code: {e.error_code}")                  # 402

Building Exception Hierarchies

For larger apps, we create a base exception for our project and build specific ones on top. This way, callers can catch all our errors with the base class, or specific ones when needed.

class AppError(Exception):
    """Base exception for our application."""
    pass

class AuthError(AppError):
    """Authentication-related errors."""
    pass

class NotFoundError(AppError):
    """Resource not found."""
    pass

class PermissionDeniedError(AuthError):
    """User doesn't have permission."""
    pass

Now we can catch broadly or narrowly:

try:
    authenticate(user)
except PermissionDeniedError:
    print("No permission")       # catches only permission issues
except AuthError:
    print("Auth problem")        # catches all auth issues
except AppError:
    print("Something went wrong") # catches all our app errors

Customizing str

We can control how our exception looks when printed by overriding __str__.

class ValidationError(Exception):
    def __init__(self, field, message):
        self.field = field
        self.message = message

    def __str__(self):
        return f"[{self.field}] {self.message}"

raise ValidationError("email", "Invalid format")
# Output: [email] Invalid format

Best Practices

  • Name ends with ErrorPaymentFailedError, not PaymentFailed or PaymentException
  • Inherit from Exception — never from BaseException
  • Keep a base class for our app — makes catching everything easy
  • Don’t go overboard — one custom exception per meaningful error scenario, not per function
  • Always call super().__init__() — so the default message behavior works

In simple language, custom exceptions are our way of speaking the language of our application. Instead of generic “something went wrong” errors, we get descriptive, catchable, data-rich error types that make debugging a breeze.