Skip to main content

Python Custom Exceptions

Introduction

Exception handling is a critical part of writing robust Python code. While Python provides many built-in exceptions to handle common error situations, there are times when you'll need to define your own exception classes to represent specific error conditions in your application. These are called custom exceptions.

In this tutorial, you'll learn:

  • Why you might want to create custom exceptions
  • How to define your own exception classes
  • Best practices for using custom exceptions
  • Real-world examples of custom exceptions in action

Why Create Custom Exceptions?

Before diving into how to create custom exceptions, let's understand why they're useful:

  1. Specificity: Custom exceptions can describe specific error conditions in your application domain.
  2. Readability: They make your code more readable by naming errors according to your application's terminology.
  3. Hierarchical organization: You can create a hierarchy of exceptions specific to your application.
  4. Meaningful error handling: They allow for more targeted except blocks.

Creating a Custom Exception

Creating a custom exception in Python is as simple as defining a new class that inherits from an existing exception class, typically Exception or one of its subclasses.

The basic syntax is:

python
class MyCustomException(Exception):
pass

Let's see a simple example:

python
class ValueTooLargeError(Exception):
"""Raised when the input value is too large"""
pass

class ValueTooSmallError(Exception):
"""Raised when the input value is too small"""
pass

# Using our custom exceptions
def validate_number(number):
if number > 100:
raise ValueTooLargeError("Number cannot be greater than 100")
if number < 10:
raise ValueTooSmallError("Number cannot be less than 10")
return "Number is valid"

# Testing the function
try:
print(validate_number(5))
except ValueTooSmallError as e:
print(f"Error: {e}")
except ValueTooLargeError as e:
print(f"Error: {e}")

Output:

Error: Number cannot be less than 10

In this example, we've created two custom exceptions that clearly communicate what went wrong with our validation.

Adding Functionality to Custom Exceptions

Custom exceptions can be more than just named errors. You can add attributes and methods to provide additional information or functionality:

python
class InsufficientFundsError(Exception):
"""Raised when a bank account has insufficient funds for a withdrawal"""

def __init__(self, balance, amount, message="Insufficient funds for withdrawal"):
self.balance = balance
self.amount = amount
self.deficit = amount - balance
self.message = message
super().__init__(self.message)

def __str__(self):
return f"{self.message}: Tried to withdraw ${self.amount} from balance of ${self.balance}. Deficit: ${self.deficit}"

# Using our enhanced custom exception
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount

# Testing the function
try:
new_balance = withdraw(50, 75)
except InsufficientFundsError as e:
print(e)
print(f"You need ${e.deficit} more in your account")

Output:

Insufficient funds for withdrawal: Tried to withdraw $75 from balance of $50. Deficit: $25
You need $25 more in your account

This custom exception not only tells us what went wrong but provides useful contextual information about the error.

Creating an Exception Hierarchy

You can create a hierarchy of custom exceptions to represent different categories of errors in your application:

python
class DatabaseError(Exception):
"""Base class for all database-related exceptions"""
pass

class ConnectionError(DatabaseError):
"""Raised when there's an error connecting to the database"""
pass

class QueryError(DatabaseError):
"""Raised when there's an error in a database query"""

def __init__(self, query, message="Invalid query"):
self.query = query
self.message = message
super().__init__(self.message)

# Using the exception hierarchy
def execute_query(query):
if "SELECT" not in query.upper():
raise QueryError(query, "Query must be a SELECT statement")

# Simulate successful query execution
return ["result1", "result2"]

# Example usage
try:
result = execute_query("UPDATE users SET active=1")
except QueryError as e:
print(f"Query Error: {e.message}")
print(f"Problem query: {e.query}")
except DatabaseError:
print("A general database error occurred")

Output:

Query Error: Query must be a SELECT statement
Problem query: UPDATE users SET active=1

The advantage here is that we can catch specific types of database errors or use except DatabaseError to catch any database-related exception.

Best Practices for Custom Exceptions

When creating and using custom exceptions, keep these best practices in mind:

  1. Naming Convention: Name your exception classes with an "Error" suffix (e.g., ValueTooLargeError).

  2. Inheritance: Inherit from the most specific built-in exception that makes sense, or from Exception if no specific one fits.

  3. Documentation: Include docstrings explaining when the exception is raised.

  4. Message Clarity: Provide clear error messages that help identify and fix the problem.

  5. Additional Context: Consider adding attributes that provide more information about the error.

  6. Exception Hierarchies: Create a hierarchy that matches your application's structure.

Real-World Example: Form Validation

Let's see a more practical example of using custom exceptions for a form validation system:

python
class ValidationError(Exception):
"""Base exception for all validation errors"""
pass

class RequiredFieldError(ValidationError):
"""Raised when a required field is missing"""
def __init__(self, field_name):
self.field_name = field_name
self.message = f"Required field '{field_name}' is missing"
super().__init__(self.message)

class InvalidEmailError(ValidationError):
"""Raised when an email is not valid"""
def __init__(self, email):
self.email = email
self.message = f"'{email}' is not a valid email address"
super().__init__(self.message)

class PasswordTooWeakError(ValidationError):
"""Raised when a password doesn't meet strength requirements"""
def __init__(self, reason):
self.reason = reason
self.message = f"Password too weak: {reason}"
super().__init__(self.message)

def validate_form(form_data):
# Check required fields
required_fields = ['username', 'email', 'password']
for field in required_fields:
if field not in form_data or not form_data[field]:
raise RequiredFieldError(field)

# Validate email format (simple check for demonstration)
if '@' not in form_data['email'] or '.' not in form_data['email']:
raise InvalidEmailError(form_data['email'])

# Check password strength
if len(form_data['password']) < 8:
raise PasswordTooWeakError("Password must be at least 8 characters long")

if not any(c.isdigit() for c in form_data['password']):
raise PasswordTooWeakError("Password must contain at least one number")

return "Form validation successful"

# Example usage
form_submissions = [
{'username': 'user1', 'password': 'pass123'}, # Missing email
{'username': 'user2', 'email': 'invalid-email', 'password': 'pass123'}, # Invalid email
{'username': 'user3', 'email': '[email protected]', 'password': 'weak'}, # Weak password
{'username': 'user4', 'email': '[email protected]', 'password': 'strongpass123'} # Valid
]

for i, form_data in enumerate(form_submissions, 1):
print(f"\nValidating form {i}:")
try:
result = validate_form(form_data)
print(result)
except ValidationError as e:
print(f"Validation failed: {e}")

Output:

Validating form 1:
Validation failed: Required field 'email' is missing

Validating form 2:
Validation failed: 'invalid-email' is not a valid email address

Validating form 3:
Validation failed: Password too weak: Password must be at least 8 characters long

Validating form 4:
Form validation successful

This example shows how custom exceptions can create a clean, structured approach to form validation, making the code more readable and maintainable.

Summary

Custom exceptions are a powerful feature in Python that allow you to:

  • Create application-specific error types
  • Provide more detailed information about errors
  • Structure your error handling in a logical way
  • Make your code more readable and self-documenting

By defining your own exception classes, you can make your error handling more precise and your code more robust. Remember that the goal of exceptions is not just to report errors but to provide enough information to understand and potentially recover from them.

Additional Resources

To further enhance your understanding of Python exceptions, check out these resources:

  1. Python Documentation on User-defined Exceptions
  2. PEP 8 - Style Guide for Python Code (see section on exception naming)

Exercises

  1. Create a custom FileFormatError that provides information about expected formats and the format that was actually provided.

  2. Implement a NetworkError hierarchy with subclasses for different types of network-related errors.

  3. Enhance the form validation example by adding more validations and custom exceptions.

  4. Create a custom exception for a calculator application that provides information about what operation failed and why.

  5. Design a logging system that captures custom exceptions and records detailed information about them.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)