Custom Exceptions
Learn how to create your own exception classes in Python to make error handling cleaner and more meaningful.
Custom Exceptions
Python's built-in exceptions — ValueError, TypeError, FileNotFoundError — are general purpose. They describe what went wrong technically, but they do not say anything about your specific application.
When you build a real project, you want errors that make sense in your context:
raise ValueError("invalid value") # generic — tells you nothing about your app
raise InsufficientFundsError(amount=50) # specific — tells you exactly what happenedCustom exceptions make your code more readable, easier to debug, and easier to handle precisely.
Creating a basic custom exception
All you need is a class that inherits from Exception:
class InsufficientFundsError(Exception):
passThat is a complete, working custom exception. You can raise and catch it just like any built-in:
class InsufficientFundsError(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError("Not enough funds in your account.")
return balance - amount
try:
withdraw(100, 200)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")Output:
Transaction failed: Not enough funds in your account.Adding useful information
A plain pass exception works but does not carry much context. Add an __init__ to store extra data that helps diagnose the problem:
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
self.shortfall = amount - balance
super().__init__(
f"Cannot withdraw ${amount:.2f}. "
f"Balance is ${balance:.2f}. "
f"You are ${self.shortfall:.2f} short."
)
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
try:
new_balance = withdraw(100.00, 250.00)
except InsufficientFundsError as e:
print(e)
print(f"Shortfall: ${e.shortfall:.2f}")Output:
Cannot withdraw $250.00. Balance is $100.00. You are $150.00 short.
Shortfall: $150.00The exception carries structured data — not just a message string. You can access e.balance, e.amount, and e.shortfall in your handler and make decisions based on them.
Always call super().__init__(message) to pass the message to the parent Exception class. This ensures the message shows up correctly in tracebacks and when you print the exception.
Building an exception hierarchy
For a real application, you want a tree of exceptions — a base exception for your app, with specific exceptions underneath. This lets you catch broadly or narrowly depending on what you need.
# base exception for the entire banking module
class BankingError(Exception):
"""Base class for all banking exceptions."""
pass
# specific exceptions
class InsufficientFundsError(BankingError):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
self.shortfall = amount - balance
super().__init__(
f"Cannot withdraw ${amount:.2f}. "
f"Balance: ${balance:.2f}. "
f"Shortfall: ${self.shortfall:.2f}."
)
class AccountLockedError(BankingError):
def __init__(self, account_id, reason):
self.account_id = account_id
self.reason = reason
super().__init__(
f"Account {account_id} is locked. Reason: {reason}"
)
class TransactionLimitError(BankingError):
def __init__(self, amount, limit):
self.amount = amount
self.limit = limit
super().__init__(
f"Transaction of ${amount:.2f} exceeds daily limit of ${limit:.2f}."
)
class InvalidAccountError(BankingError):
def __init__(self, account_id):
self.account_id = account_id
super().__init__(f"Account {account_id} does not exist.")Now you can catch at different levels of specificity:
def process_transaction(account_id, amount):
# simulate some checks
if account_id not in accounts:
raise InvalidAccountError(account_id)
if accounts[account_id]["locked"]:
raise AccountLockedError(account_id, "suspicious activity detected")
if amount > 5000:
raise TransactionLimitError(amount, limit=5000)
if amount > accounts[account_id]["balance"]:
raise InsufficientFundsError(accounts[account_id]["balance"], amount)
# catch a specific exception
try:
process_transaction("ACC001", 10000)
except InsufficientFundsError as e:
print(f"Funds error: {e}")
print(f"You need ${e.shortfall:.2f} more.")
# catch any banking exception
try:
process_transaction("ACC999", 100)
except BankingError as e:
print(f"Banking error: {e}")
# catch everything from most specific to least specific
try:
process_transaction("ACC001", 10000)
except InsufficientFundsError as e:
print("Not enough funds.")
except AccountLockedError as e:
print(f"Account locked: {e.reason}")
except TransactionLimitError as e:
print(f"Over the limit by ${e.amount - e.limit:.2f}")
except BankingError as e:
print(f"General banking error: {e}")The base exception BankingError is the safety net. If a new specific exception is added later and your code does not handle it yet, catching BankingError ensures it does not slip through unhandled.
Adding a str method
You can control exactly how your exception prints by defining __str__:
class ValidationError(Exception):
def __init__(self, field, value, message):
self.field = field
self.value = value
self.message = message
super().__init__(message)
def __str__(self):
return f"[ValidationError] Field '{self.field}' = '{self.value}': {self.message}"
try:
raise ValidationError("email", "not-an-email", "must contain @ symbol")
except ValidationError as e:
print(e)Output:
[ValidationError] Field 'email' = 'not-an-email': must contain @ symbolA real example — form validation
A complete validation system using custom exceptions:
class ValidationError(Exception):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class MissingFieldError(ValidationError):
def __init__(self, field):
super().__init__(field, "this field is required")
class InvalidFormatError(ValidationError):
def __init__(self, field, expected_format):
self.expected_format = expected_format
super().__init__(field, f"invalid format — expected {expected_format}")
class ValueOutOfRangeError(ValidationError):
def __init__(self, field, value, min_val, max_val):
self.value = value
self.min_val = min_val
self.max_val = max_val
super().__init__(
field,
f"value {value} is out of range ({min_val}-{max_val})"
)
def validate_user(data):
errors = []
# check required fields
for field in ["name", "email", "age"]:
if field not in data or not data[field]:
errors.append(MissingFieldError(field))
# check email format
if "email" in data and data["email"]:
if "@" not in data["email"] or "." not in data["email"]:
errors.append(InvalidFormatError("email", "user@example.com"))
# check age range
if "age" in data and data["age"]:
try:
age = int(data["age"])
if age < 18 or age > 120:
errors.append(ValueOutOfRangeError("age", age, 18, 120))
except ValueError:
errors.append(InvalidFormatError("age", "a whole number"))
return errors
# test it
user_data = {
"name": "Ali",
"email": "not-an-email",
"age": "15"
}
errors = validate_user(user_data)
if errors:
print("Validation failed:")
for error in errors:
print(f" - {error}")
else:
print("Validation passed.")Output:
Validation failed:
- email: invalid format — expected user@example.com
- age: value 15 is out of range (18-120)Each error is a structured object — you know the field, the type of problem, and the message. This is far more useful than a plain string.
When to create custom exceptions
| Situation | Create custom exception? |
|---|---|
| Your error is specific to your domain | Yes |
| You want to carry extra data with the error | Yes |
| You want callers to handle your errors separately | Yes |
| You are building a library or package | Yes |
| A built-in exception describes it perfectly | No |
| It is a one-off script | Probably not |
Summary
| Concept | Example |
|---|---|
| Basic custom exception | class MyError(Exception): pass |
| With custom message | super().__init__("message") |
| With extra data | self.field = field in __init__ |
| Exception hierarchy | Inherit from a base custom exception |
| Catch specific | except InsufficientFundsError |
| Catch all custom | except BankingError |
| Access extra data | e.shortfall, e.field |