Built-in exceptions are too generic for business logic:
ValueError could mean anything (negative age, invalid email, wrong format...)class InvalidAgeError(Exception):
pass
# Raising it
raise InvalidAgeError('Age cannot be negative') def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f'{field}: {message}')
try:
raise ValidationError('email', 'Invalid format')
except ValidationError as e:
print(e) # email: Invalid format
print(e.field) # 'email'
print(e.message) # 'Invalid format'class AppError(Exception):
pass
# Sub-categories
class NetworkError(AppError):
pass
class DatabaseError(AppError):
pass
# Specific errors
class ConnectionTimeoutError(NetworkError):
pass
class QueryError(DatabaseError):
pass
# Caller can catch at any level
try:
raise ConnectionTimeoutError('Server not responding')
except ConnectionTimeoutError:
print('Handle timeout specifically')
except NetworkError:
print('Handle any network issue')
except AppError:
print('Handle any app error') def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f'Cannot withdraw {amount}. Balance: {balance}')
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError('Amount must be positive')
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
try:
acc = BankAccount(100)
acc.withdraw(150)
except InsufficientFundsError as e:
print(e) # Cannot withdraw 150. Balance: 100
print(f'Short by: {e.amount - e.balance}')class MyError(Exception): pass
# ❌ DON'T: Inherit from BaseException
# (catches SystemExit, KeyboardInterrupt, GeneratorExit)
class MyError(BaseException): pass
# ✅ DO: Provide useful default messages
# ✅ DO: Group related exceptions in a hierarchyCreate an exception hierarchy: `AppError` (base), `AuthError(AppError)`, `InvalidCredentialsError(AuthError)`, `PermissionDeniedError(AuthError)`. Add a custom `status_code` attribute to all. Create a function `login(username, password, role)` that raises `InvalidCredentialsError` (401) if username/password is wrong, or `PermissionDeniedError` (403) if role is not 'admin'.