Raise Specific Errors and Define Your Own If Needed
Raise Specific Errors and Define Your Own If Needed
The rule Raise specific errors and define your own if needed means that you should use the most appropriate exception type for each error situation, choosing from built-in exceptions when they fit, and creating custom exception classes when they don’t.
Why specific errors? Specific exceptions precisely indicate what went wrong, making code more maintainable. When you catch a ValueError, you know the problem is with the value of the data. When you catch a FileNotFoundError, you know a file is missing. Generic exceptions like Exception or bare except: clauses hide the actual problem, making debugging and error handling much more difficult.
Built-in exceptions to use: Python provides many specific exception types. Choose the most appropriate one:
ValueError- often the most appropriate when called with “bad” data (wrong value, invalid format)TypeError- for unsupported types of data (wrong type passed to function)FileNotFoundError- when a file or directory cannot be foundPermissionError- when an operation is not permitted due to insufficient permissionsKeyError- when a dictionary key is missingIndexError- when a sequence index is out of rangeAttributeError- when an attribute (variable or function) doesn’t exist on the object, e.g., callingx.append('Bob')butxis a dictionary. Dictionaries don’t understand how toappend()in Python.- And many more specific exceptions for different scenarios
When to create custom exceptions: When built-in exceptions don’t accurately represent your domain-specific errors, create your own exception classes. Custom exceptions make it clear that an error is specific to your application’s domain, not a general programming error. For example, if you’re building a payment system, a PaymentProcessingError or InsufficientFundsError is more meaningful than a generic ValueError.
Benefits
- Precise error identification: callers can catch specific exceptions and handle them appropriately
- Better maintainability: developers can quickly understand what went wrong
- Improved debugging: specific error types make it easier to locate and fix issues
- Clearer code intent: the exception type itself documents what can go wrong
- Enables selective error handling: callers can catch only the exceptions they know how to handle
Red flags (violations):
- Raising generic
Exceptioninstead of specific exception types - Using
ValueErrorfor everything, even whenTypeErroror other exceptions are more appropriate - Catching all exceptions with bare
except:orexcept Exception:without distinguishing types
- Using string error messages instead of exceptions when an exception is more appropriate
- Creating custom exceptions that don’t add meaningful information beyond built-in exceptions
Example 1 - Using generic Exception instead of specific exceptions
Problematic code
def validate_age(age):
if age < 0:
raise Exception("Age cannot be negative")
if not isinstance(age, int):
raise Exception("Age must be an integer")
return age
def process_user_data(user_id):
try:
user = fetch_user_from_database(user_id)
return user
except Exception:
return None # Caller doesn't know what went wrong
Problem: These functions use generic Exception instead of specific exceptions. Callers can’t distinguish between different error types, making it impossible to handle specific errors appropriately. For example, a caller can’t tell if read_config_file failed because the file was missing (FileNotFoundError) or because of a permission issue (PermissionError), so they can’t respond appropriately.
Fixed using specific exceptions
def validate_age(age):
if not isinstance(age, int):
raise TypeError(f"Age must be an integer, got {type(age).__name__}")
if age < 0:
raise ValueError(f"Age cannot be negative, got {age}")
return age
def process_user_data(user_id):
try:
user = fetch_user_from_database(user_id)
return user
except ConnectionError:
# Network issue - caller might want to retry
raise ConnectionError("Could not connect to database.") from e
except ValueError as e:
# Invalid user ID format - different from network error
raise ValueError(f"Invalid user_id format: {user_id}") from e
Why this is better: Specific exceptions allow callers to handle different error types appropriately. For example, a caller can catch FileNotFoundError to prompt for a different file, or catch PermissionError to display a permission-related message. The exception type itself communicates what went wrong.
Example 2 - Creating custom exceptions for domain-specific errors
Problematic code
def process_payment(card_number: str, amount: float) -> bool:
if not card_number or len(card_number) < 13:
raise ValueError("Invalid card number")
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > 10000:
raise ValueError("Amount exceeds daily limit")
# Check if card is expired
if is_card_expired(card_number):
raise ValueError("Card is expired")
# Check if insufficient funds
balance = get_account_balance(card_number)
if balance < amount:
raise ValueError("Insufficient funds")
# Process payment
return True
Problem: All errors raise ValueError, even though they represent fundamentally different problems. A caller can’t distinguish between “invalid card format”, “card expired”, “insufficient funds”, and “amount exceeds limit” - all are treated as generic value errors. This makes it difficult to handle different payment errors appropriately (e.g., retry for insufficient funds vs. reject for expired card). You could inspect the error message, but that would be using a magic literal.
Fixed by creating custom exceptions
# Define custom exceptions for payment domain
class PaymentError(Exception):
"""Base exception for payment-related errors"""
pass
class InvalidCardError(PaymentError):
"""Raised when card number format is invalid"""
pass
class CardExpiredError(PaymentError):
"""Raised when card has expired"""
pass
class InsufficientFundsError(PaymentError):
"""Raised when account has insufficient funds"""
pass
class AmountExceedsLimitError(PaymentError):
"""Raised when payment amount exceeds allowed limit"""
pass
def process_payment(card_number: str, amount: float) -> bool:
if not card_number or len(card_number) < 13:
raise InvalidCardError(f"Invalid card number format: {card_number}")
if amount <= 0:
raise ValueError("Amount must be positive") # Still ValueError - general validation
if amount > 10000:
raise AmountExceedsLimitError(f"Amount {amount} exceeds daily limit of 10000")
# Check if card is expired
if is_card_expired(card_number):
raise CardExpiredError("Card has expired")
# Check if insufficient funds
balance = get_account_balance(card_number)
if balance < amount:
raise InsufficientFundsError(f"Insufficient funds: balance {balance}, required {amount}")
# Process payment
return True
# Caller can now handle specific errors appropriately
def handle_payment_request(card_number: str, amount: float):
try:
process_payment(card_number, amount)
print("Payment successful!")
except CardExpiredError:
print("Your card has expired. Please use a different card.")
except InsufficientFundsError:
print("Insufficient funds. Please try a smaller amount.")
except AmountExceedsLimitError:
print("Payment amount exceeds daily limit. Please contact support.")
except InvalidCardError:
print("Invalid card number. Please check and try again.")
except PaymentError:
# Catch any other payment-related errors
print("Payment processing failed. Please try again later.")
Why this is better: Custom exceptions clearly communicate domain-specific errors. Callers can catch specific exceptions (InsufficientFundsError, CardExpiredError) and handle them appropriately, or catch the base PaymentError to handle any payment-related error. The exception hierarchy also allows for selective handling: catch PaymentError for all payment issues, or catch specific subclasses for granular control.
How to apply this rule
Choose the most appropriate built-in exception. When raising an error, use the most specific built-in exception that accurately describes the problem:
- Use
ValueErrorfor invalid values or data formats - Use
TypeErrorfor wrong types - Use
FileNotFoundErrorfor missing files - Use
PermissionErrorfor permission issues - Use
KeyErrorfor missing dictionary keys - Use
IndexErrorfor out-of-range indices - And so on…
- Use
Create custom exceptions when built-in ones don’t fit. When your error is domain-specific and doesn’t match any built-in exception, create your own:
class DataProcessError(Exception): passCreate a hierarchy if needed:
class PaymentError(Exception): pass class InsufficientFundsError(PaymentError): passDon’t use generic
Exception. Avoid raisingExceptiondirectly - it’s too generic and doesn’t help callers handle errors appropriately.Don’t misuse
ValueErrorfor everything. WhileValueErroris common, don’t use it whenTypeError,FileNotFoundError, or other exceptions are more appropriate.Catch specific exceptions when possible. When catching exceptions, catch specific types rather than generic
Exception:try: process_data() except FileNotFoundError: # Handle missing file except ValueError: # Handle invalid dataUse exception hierarchies for domain errors. Create a base exception class for your domain, then subclass it for specific cases. This allows callers to catch either specific errors or all domain errors:
try: process_payment() except InsufficientFundsError: # Handle specific case except PaymentError: # Handle any payment error
Knowledge Check
- You’re writing a function that validates user input. The function receives a string when it expects an integer. What exception should you raise?
Exception("Expected integer")ValueError("Expected integer")TypeError("Expected integer")AttributeError("Expected integer")
Answer
3 —TypeError`is the most appropriate exception for when the wrong type is passed to a function.ValueErrorwould be for an integer with an invalid value (like a negative age), not for the wrong type entirely. - Your function reads a configuration file, but the file doesn’t exist. What exception should you raise?
ValueError("File not found")FileNotFoundError("config.txt")Exception("File missing")KeyError("config.txt")
Answer
2 —FileNotFoundErroris the specific built-in exception for missing files. It's more precise than `ValueError` or generic `Exception`, and allows callers to handle file-not-found errors specifically. - You’re building a payment processing system and need to indicate when a payment fails due to insufficient funds. The built-in exceptions don’t accurately represent this domain-specific error. What should you do?
- Raise
ValueError("Insufficient funds")since it’s a value problem - Create a custom exception class like
class InsufficientFundsError(Exception) - Raise
Exception("Payment failed")to be generic - Return
Falseinstead of raising an exception
Answer
2 — when built-in exceptions don't accurately represent your domain-specific errors, create custom exception classes. This makes the error type clear and allows callers to catch and handleInsufficientFundsErrorspecifically, which is more meaningful than a genericValueError. - Raise
- Which of the following is a red flag that violates the “raise specific errors” principle?
- Using
TypeErrorwhen a function receives the wrong type - Creating a custom
DataProcessErrorexception for data processing failures - Raising
Exceptionfor all errors instead of specific exception types - Using
FileNotFoundErrorwhen a file is missing
Answer
3 — raising genericExceptionfor all errors violates the principle because it doesn't help callers distinguish between different error types. Specific exceptions likeValueError,TypeError,FileNotFoundError, or custom exceptions allow for precise error handling. - Using