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
Error—PaymentFailedError, notPaymentFailedorPaymentException - Inherit from
Exception— never fromBaseException - 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.