Exceptions
Custom Exceptions
Domain-Specific Errors
Your banking app needs to signal "insufficient funds". Using generic ValueError
loses meaning. A custom InsufficientFundsError carries the balance and requested
amount - meaningful error information for the specific domain.
Simple custom exception
Create your own exception type.
# Define basic custom exception
def main():
# Simple custom exception
class InsufficientFundsError(Exception):
"""Raised when account balance is insufficient"""
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError("Not enough funds")
return balance - amount
print("Bank withdrawal:\n")
# Successful withdrawal
try:
new_balance = withdraw(100, 30)
print(f" Withdrew $30: Balance ${new_balance}")
except InsufficientFundsError as e:
print(f" Error: {e}")
# Failed withdrawal
try:
new_balance = withdraw(100, 150)
print(f" Withdrew $150: Balance ${new_balance}")
except InsufficientFundsError as e:
print(f" Error: {e}")
# Multiple custom exceptions
class InvalidUsernameError(Exception):
pass
class InvalidEmailError(Exception):
pass
class InvalidAgeError(Exception):
pass
def validate_user(username, email, age):
if len(username) < 3:
raise InvalidUsernameError("Username too short")
if "@" not in email:
raise InvalidEmailError("Invalid email format")
if age < 18:
raise InvalidAgeError("Must be 18 or older")
return True
print("\nUser validation:")
users = [
("alice", "alice@example.com", 25),
("ab", "alice@example.com", 25),
("alice", "invalid", 25),
("alice", "alice@example.com", 15)
]
for username, email, age in users:
try:
validate_user(username, email, age)
print(f" {username}: Valid")
except (InvalidUsernameError, InvalidEmailError, InvalidAgeError) as e:
print(f" {username}: {type(e).__name__} - {e}")
# Custom exception with default message
class ConfigurationError(Exception):
"""Raised when configuration is invalid"""
def __init__(self, message="Invalid configuration"):
super().__init__(message)
try:
raise ConfigurationError()
except ConfigurationError as e:
print(f"\nDefault message: {e}")
try:
raise ConfigurationError("Missing API key")
except ConfigurationError as e:
print(f"Custom message: {e}")
if __name__ == "__main__":
main()
Inherit from Exception. Add pass for minimal exception.
Exception with data
Include relevant context in the exception.
# Custom exception with additional data
def main():
# Custom exception with fields
class ValidationError(Exception):
def __init__(self, message, field_name, invalid_value):
super().__init__(message)
self.field_name = field_name
self.invalid_value = invalid_value
def validate_age(age):
if not isinstance(age, int):
raise ValidationError(
"Age must be an integer",
field_name="age",
invalid_value=age
)
if age < 0:
raise ValidationError(
"Age cannot be negative",
field_name="age",
invalid_value=age
)
if age > 150:
raise ValidationError(
"Age is unrealistic",
field_name="age",
invalid_value=age
)
return True
print("Age validation with context:\n")
test_ages = [25, "invalid", -5, 200]
for age in test_ages:
try:
validate_age(age)
print(f" {age}: Valid")
except ValidationError as e:
print(f" Field: {e.field_name}")
print(f" Value: {e.invalid_value}")
print(f" Error: {e}")
print()
# Exception with error code
class DatabaseError(Exception):
def __init__(self, message, query, error_code):
super().__init__(message)
self.query = query
self.error_code = error_code
def execute_query(query):
if "DROP" in query:
raise DatabaseError(
"DROP not allowed",
query=query,
error_code=403
)
if "invalid" in query:
raise DatabaseError(
"Syntax error",
query=query,
error_code=400
)
return "SUCCESS"
print("\nDatabase queries:")
queries = [
"SELECT * FROM users",
"DROP TABLE users",
"invalid syntax"
]
for query in queries:
try:
result = execute_query(query)
print(f" '{query}': {result}")
except DatabaseError as e:
print(f" Error code: {e.error_code}")
print(f" Query: {e.query}")
print(f" Message: {e}")
print()
# Exception with multiple context fields
class PaymentError(Exception):
def __init__(self, message, amount, account_id, transaction_id):
super().__init__(message)
self.amount = amount
self.account_id = account_id
self.transaction_id = transaction_id
def __str__(self):
return (f"{super().__str__()} "
f"[Txn:{self.transaction_id}, "
f"Account:{self.account_id}, "
f"Amount:${self.amount}]")
try:
raise PaymentError(
"Insufficient funds",
amount=100.00,
account_id="ACC123",
transaction_id="TXN456"
)
except PaymentError as e:
print("Payment failure:")
print(f" {e}")
print(f" Amount: ${e.amount}")
print(f" Account: {e.account_id}")
print(f" Transaction: {e.transaction_id}")
if __name__ == "__main__":
main()
Add __init__ with parameters. Store data as attributes.
Exception hierarchy
Organize related exceptions into a hierarchy.
# Exception hierarchy
def main():
# Create exception hierarchy
class AppError(Exception):
"""Base exception for this application"""
pass
class ValidationError(AppError):
"""Validation failed"""
pass
class AuthenticationError(AppError):
"""Authentication failed"""
pass
class AuthorizationError(AppError):
"""Authorization failed"""
pass
class InvalidCredentialsError(AuthenticationError):
"""Specific auth error"""
pass
def process_request(username, password, action):
# Authentication
if not username or not password:
raise InvalidCredentialsError("Missing credentials")
if password != "secret":
raise AuthenticationError("Wrong password")
# Authorization
if action == "delete" and username != "admin":
raise AuthorizationError(f"{username} cannot delete")
# Validation
if not action:
raise ValidationError("Action required")
return "SUCCESS"
print("Request processing:\n")
requests = [
("alice", "secret", "read"),
("", "secret", "read"),
("bob", "wrong", "read"),
("alice", "secret", "delete"),
("admin", "secret", "delete")
]
for user, pwd, action in requests:
try:
result = process_request(user, pwd, action)
print(f" {user} - {action}: {result}")
except InvalidCredentialsError as e:
print(f" {user} - {action}: Invalid credentials")
except AuthenticationError as e:
print(f" {user} - {action}: Auth failed")
except AuthorizationError as e:
print(f" {user} - {action}: Not authorized")
except ValidationError as e:
print(f" {user} - {action}: Validation error")
# Catch at different levels
print("\nCatching at different levels:")
try:
raise InvalidCredentialsError("Bad login")
except AuthenticationError as e:
# Catches InvalidCredentialsError (subclass)
print(f" Caught as AuthenticationError: {type(e).__name__}")
try:
raise AuthorizationError("No permission")
except AppError as e:
# Catches any AppError subclass
print(f" Caught as AppError: {type(e).__name__}")
# Module-specific hierarchy
class DataError(Exception):
"""Base for data-related errors"""
pass
class ParseError(DataError):
"""Failed to parse data"""
pass
class FormatError(DataError):
"""Invalid data format"""
pass
class EncodingError(DataError):
"""Encoding problem"""
pass
def process_data(data, format_type):
if format_type not in ["json", "xml", "csv"]:
raise FormatError(f"Unsupported format: {format_type}")
if not data:
raise ParseError("Empty data")
if not data.startswith("{"):
raise EncodingError("Invalid encoding")
return "PARSED"
print("\nData processing:")
datasets = [
('{"name": "Alice"}', "json"),
("", "json"),
("not json", "json"),
('{"name": "Bob"}', "yaml")
]
for data, fmt in datasets:
try:
result = process_data(data, fmt)
print(f" {fmt}: {result}")
except ParseError as e:
print(f" {fmt}: Parse error - {e}")
except FormatError as e:
print(f" {fmt}: Format error - {e}")
except EncodingError as e:
print(f" {fmt}: Encoding error - {e}")
except DataError as e:
# Catch any other DataError
print(f" {fmt}: Data error - {e}")
if __name__ == "__main__":
main()
Base exception for module. Specialized subclasses for specific errors.
Override __str__
Customize exception's string representation.
# Override __str__ for custom formatting
def main():
# Custom __str__ method
class ProductNotFoundError(Exception):
def __init__(self, product_id, category=None):
self.product_id = product_id
self.category = category
def __str__(self):
msg = f"Product {self.product_id} not found"
if self.category:
msg += f" in category '{self.category}'"
return msg
print("Product lookup:\n")
try:
raise ProductNotFoundError(123, "Electronics")
except ProductNotFoundError as e:
print(f" Error: {e}")
print(f" Product ID: {e.product_id}")
print(f" Category: {e.category}")
try:
raise ProductNotFoundError(456)
except ProductNotFoundError as e:
print(f"\n Error: {e}")
print(f" Product ID: {e.product_id}")
# Rich error messages
class OrderError(Exception):
def __init__(self, order_id, items, total, reason):
self.order_id = order_id
self.items = items
self.total = total
self.reason = reason
def __str__(self):
return (f"Order #{self.order_id} failed: {self.reason}\n"
f" Items: {self.items}\n"
f" Total: ${self.total:.2f}")
print("\nOrder processing:")
try:
raise OrderError(
order_id=789,
items=["Laptop", "Mouse"],
total=1050.00,
reason="Payment declined"
)
except OrderError as e:
print(f"{e}")
# Exception with repr
class ApiError(Exception):
def __init__(self, status_code, endpoint, message):
self.status_code = status_code
self.endpoint = endpoint
self.message = message
def __str__(self):
return f"[{self.status_code}] {self.message}"
def __repr__(self):
return (f"ApiError(status_code={self.status_code}, "
f"endpoint='{self.endpoint}', "
f"message='{self.message}')")
error = ApiError(404, "/api/users/123", "User not found")
print("\nAPI error:")
print(f" str: {str(error)}")
print(f" repr: {repr(error)}")
# Format with multiple fields
class ValidationError(Exception):
def __init__(self, errors):
self.errors = errors
def __str__(self):
if len(self.errors) == 1:
field, msg = next(iter(self.errors.items()))
return f"Validation failed: {field} - {msg}"
lines = ["Multiple validation errors:"]
for field, msg in self.errors.items():
lines.append(f" - {field}: {msg}")
return "\n".join(lines)
print("\nValidation errors:")
try:
raise ValidationError({"age": "Must be positive"})
except ValidationError as e:
print(f"{e}\n")
try:
raise ValidationError({
"username": "Too short",
"email": "Invalid format",
"age": "Must be 18+"
})
except ValidationError as e:
print(f"{e}")
# Exception with timestamp
from datetime import datetime
class TimestampedError(Exception):
def __init__(self, message):
self.message = message
self.timestamp = datetime.now()
def __str__(self):
time_str = self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
return f"[{time_str}] {self.message}"
print("\nTimestamped error:")
try:
raise TimestampedError("Connection lost")
except TimestampedError as e:
print(f" {e}")
print(f" Time: {e.timestamp}")
if __name__ == "__main__":
main()
def __str__(self): controls what print(exception) shows.
When to create custom exceptions
Guidelines for when it's worth it.
# When to create custom exceptions
def main():
# Domain-specific errors
class ShoppingCartError(Exception):
"""Base for shopping cart errors"""
pass
class ItemNotInCartError(ShoppingCartError):
def __init__(self, item_id):
self.item_id = item_id
super().__init__(f"Item {item_id} not in cart")
class CartLimitExceededError(ShoppingCartError):
def __init__(self, limit, attempted):
self.limit = limit
self.attempted = attempted
super().__init__(
f"Cart limit {limit} exceeded: tried to add {attempted}"
)
class Cart:
def __init__(self, max_items=5):
self.items = []
self.max_items = max_items
def add(self, item):
if len(self.items) >= self.max_items:
raise CartLimitExceededError(
self.max_items,
len(self.items) + 1
)
self.items.append(item)
def remove(self, item_id):
for item in self.items:
if item["id"] == item_id:
self.items.remove(item)
return
raise ItemNotInCartError(item_id)
print("Shopping cart with custom exceptions:\n")
cart = Cart(max_items=3)
# Add items
try:
cart.add({"id": 1, "name": "Laptop"})
cart.add({"id": 2, "name": "Mouse"})
cart.add({"id": 3, "name": "Keyboard"})
print(f" Added 3 items: {len(cart.items)} in cart")
except CartLimitExceededError as e:
print(f" Error: {e}")
# Try to exceed limit
try:
cart.add({"id": 4, "name": "Monitor"})
except CartLimitExceededError as e:
print(f" Error: {e}")
print(f" Limit: {e.limit}, Attempted: {e.attempted}")
# Remove item
try:
cart.remove(2)
print(f"\n Removed item 2: {len(cart.items)} in cart")
except ItemNotInCartError as e:
print(f" Error: {e}")
# Try to remove non-existent
try:
cart.remove(999)
except ItemNotInCartError as e:
print(f" Error: {e}")
print(f" Item ID: {e.item_id}")
# API-style exceptions
class ApiException(Exception):
def __init__(self, status_code, message, details=None):
self.status_code = status_code
self.message = message
self.details = details
super().__init__(message)
class BadRequestError(ApiException):
def __init__(self, message, details=None):
super().__init__(400, message, details)
class NotFoundError(ApiException):
def __init__(self, resource, resource_id):
super().__init__(
404,
f"{resource} not found",
{"resource": resource, "id": resource_id}
)
class UnauthorizedError(ApiException):
def __init__(self, message="Unauthorized"):
super().__init__(401, message)
def api_request(endpoint, user_id, auth_token):
if not auth_token:
raise UnauthorizedError("Missing auth token")
if auth_token != "valid_token":
raise UnauthorizedError("Invalid token")
if not user_id:
raise BadRequestError(
"Missing required parameter",
{"missing_field": "user_id"}
)
if user_id not in [1, 2, 3]:
raise NotFoundError("User", user_id)
return {"user_id": user_id, "data": "..."}
print("\nAPI requests:")
requests = [
("/users/1", 1, "valid_token"),
("/users/999", 999, "valid_token"),
("/users/1", None, "valid_token"),
("/users/1", 1, None),
("/users/1", 1, "bad_token")
]
for endpoint, user_id, token in requests:
try:
result = api_request(endpoint, user_id, token)
print(f" {endpoint}: OK")
except UnauthorizedError as e:
print(f" {endpoint}: {e.status_code} {e.message}")
except NotFoundError as e:
print(f" {endpoint}: {e.status_code} {e.message}")
print(f" Details: {e.details}")
except BadRequestError as e:
print(f" {endpoint}: {e.status_code} {e.message}")
print(f" Details: {e.details}")
except ApiException as e:
# Catch-all for other API errors
print(f" {endpoint}: {e.status_code} {e.message}")
if __name__ == "__main__":
main()
Create custom exceptions when you need specific handling or extra data.
Exercise: practical.py
Build a web API with domain-specific exceptions