- What are user defined exceptions — definition and purpose
- Why create custom exceptions — benefits and use cases
- Creating custom exceptions — inheriting from Exception class
- Adding custom attributes — storing additional data
- Raising custom exceptions — using the raise keyword
- Handling custom exceptions — try-except with custom exceptions
- Exception hierarchy — organizing custom exceptions
- Best practices — writing clean custom exceptions
What are User Defined Exceptions?
User defined exceptions are custom exception classes that you create by inheriting from Python's built-in Exception class. They allow you to define application-specific error types that are meaningful to your program's domain.
Definition: A user defined exception is a class that inherits from the Exception class (or any of its subclasses) and is used to represent errors that are specific to your application's logic or domain.
💡 Key concept: User defined exceptions make your code more readable and maintainable by using meaningful error names instead of generic built-in exceptions.
Why Create Custom Exceptions?
Benefits of Custom Exceptions
Creating custom exceptions provides several important benefits:
# 1. Meaningful error names
class InsufficientFundsError(Exception):
"""Raised when a bank account has insufficient funds"""
pass
# 2. Better error messages with context
class InvalidAgeError(Exception):
def __init__(self, age, message="Age must be between 0 and 150"):
self.age = age
self.message = message
super().__init__(f"Invalid age {age}: {message}")
# 3. Organized error hierarchy
class AppError(Exception):
pass
class DatabaseError(AppError):
pass
class ValidationError(AppError):
pass
# 4. Specific error handling
try:
withdraw_money(1000)
except InsufficientFundsError as e:
# Handle insufficient funds specifically
print(f"Please add funds to your account. {e}")
except Exception as e:
# Handle other errors
print(f"Unexpected error: {e}")
Benefits:
- Readability — meaningful error names make code self-documenting
- Specificity — catch and handle specific error types
- Context — carry additional error information
- Organization — group related errors in a hierarchy
- Debugging — easier to identify and fix issues
Quick Check: What is the main benefit of using custom exceptions? (Answer: They make code more readable and allow specific error handling)
Creating a Custom Exception
Basic Custom Exception
Creating a custom exception is as simple as creating a class that inherits from Exception. The convention is to use the suffix "Error" in the class name.
# Basic custom exception (minimal)
class CustomError(Exception):
"""Custom exception for application-specific errors"""
pass
# Using the custom exception
try:
raise CustomError("Something went wrong!")
except CustomError as e:
print(f"Caught custom error: {e}")
# Another example
class NegativeNumberError(Exception):
"""Raised when a negative number is not allowed"""
pass
def check_positive(number):
if number < 0:
raise NegativeNumberError(f"Number must be positive, got {number}")
return True
try:
check_positive(-5)
except NegativeNumberError as e:
print(f"Validation failed: {e}")
Key points:
- Inherit from
Exceptionclass (or its subclasses) - Add a docstring for documentation
- Use
passfor minimal implementation - Convention: name ends with Error
- Can be raised and caught like built-in exceptions
Quick Check: What class should a custom exception inherit from? (Answer: Exception)
Adding Custom Attributes
Creating Informative Exceptions
You can add custom attributes and methods to your exception classes to carry additional information about the error.
# Custom exception with attributes
class InsufficientFundsError(Exception):
"""Raised when an account doesn't have enough funds"""
def __init__(self, balance, amount, account_id=None):
self.balance = balance
self.amount = amount
self.account_id = account_id
self.shortfall = amount - balance
message = f"Insufficient funds: balance {balance}, needed {amount}"
if account_id:
message += f" (Account: {account_id})"
super().__init__(message)
# Using the exception
try:
balance = 100
amount = 150
raise InsufficientFundsError(balance, amount, account_id="ACC-001")
except InsufficientFundsError as e:
print(f"Error: {e}")
print(f"Balance: {e.balance}")
print(f"Needed: {e.amount}")
print(f"Shortfall: {e.shortfall}")
print(f"Account: {e.account_id}")
# Output:
# Error: Insufficient funds: balance 100, needed 150 (Account: ACC-001)
# Balance: 100
# Needed: 150
# Shortfall: 50
# Account: ACC-001
Key points:
- Define
__init__method to accept custom data - Store data as attributes for later access
- Call
super().__init__(message)to set the error message - Attributes make debugging easier
Quick Check: How do you add custom data to an exception? (Answer: By defining __init__ and storing attributes)
Raising Custom Exceptions
Triggering Custom Exceptions
You raise custom exceptions the same way as built-in exceptions — using the raise keyword.
# Raising custom exceptions
class ValidationError(Exception):
pass
class AgeValidationError(ValidationError):
def __init__(self, age, message="Invalid age"):
self.age = age
super().__init__(f"{message}: {age}")
def validate_user_age(age):
if age < 0:
raise AgeValidationError(age, "Age cannot be negative")
if age > 150:
raise AgeValidationError(age, "Age cannot exceed 150")
return True
# Using validation
try:
validate_user_age(-5)
except AgeValidationError as e:
print(f"Age validation failed: {e}")
print(f"Invalid age: {e.age}")
# Raising in a function
class ConfigError(Exception):
pass
def load_config(filename):
if not filename:
raise ConfigError("Config filename cannot be empty")
# ... rest of function
Key points:
- Use raise keyword to trigger custom exceptions
- Pass arguments to the exception constructor
- Custom exceptions can be raised in any context
- They behave exactly like built-in exceptions
Quick Check: How do you raise a custom exception? (Answer: Using the raise keyword)
Handling Custom Exceptions
Catching Custom Exceptions
Custom exceptions are caught using try-except blocks, just like built-in exceptions.
# Defining custom exceptions
class DatabaseError(Exception):
pass
class ConnectionError(DatabaseError):
def __init__(self, host, port):
self.host = host
self.port = port
super().__init__(f"Failed to connect to {host}:{port}")
class QueryError(DatabaseError):
def __init__(self, query, error):
self.query = query
self.original_error = error
super().__init__(f"Query failed: {query} - {error}")
# Handling custom exceptions
def execute_query(query, host="localhost", port=5432):
try:
# Simulate database connection
if host == "unknown":
raise ConnectionError(host, port)
if "DROP TABLE" in query.upper():
raise QueryError(query, "DROP TABLE not allowed")
return "Query executed successfully"
except ConnectionError as e:
print(f"Connection failed to {e.host}:{e.port}")
return None
except QueryError as e:
print(f"Query failed: {e.query}")
return None
except DatabaseError as e:
print(f"Database error: {e}")
return None
# Testing
print(execute_query("SELECT * FROM users"))
print(execute_query("DROP TABLE users"))
print(execute_query("SELECT * FROM users", host="unknown"))
Key points:
- Custom exceptions are caught like built-in exceptions
- You can catch specific custom exceptions
- You can catch parent exceptions to handle groups
- Order matters — catch more specific exceptions first
Quick Check: Can you catch custom exceptions with try-except? (Answer: Yes, just like built-in exceptions)
Inheritance and Exception Hierarchy
Organizing Custom Exceptions
You can create a hierarchy of custom exceptions by having them inherit from each other. This allows you to catch groups of related exceptions.
# Creating an exception hierarchy
class AppError(Exception):
"""Base exception for all application errors"""
pass
class ValidationError(AppError):
"""Raised when validation fails"""
pass
class InputValidationError(ValidationError):
"""Raised when user input validation fails"""
pass
class DataValidationError(ValidationError):
"""Raised when data validation fails"""
pass
class DatabaseError(AppError):
"""Raised when database operations fail"""
pass
class ConnectionError(DatabaseError):
"""Raised when database connection fails"""
pass
class QueryError(DatabaseError):
"""Raised when a database query fails"""
pass
# Using the hierarchy
def process_data(data):
try:
# Some validation
if not isinstance(data, dict):
raise InputValidationError("Data must be a dictionary")
if "name" not in data:
raise DataValidationError("Missing 'name' field")
# Try database operation
try:
save_to_db(data)
except ConnectionError:
print("Database connection failed")
return
except QueryError:
print("Query failed")
return
except ValidationError as e:
# Catches both InputValidationError and DataValidationError
print(f"Validation error: {e}")
except DatabaseError as e:
# Catches both ConnectionError and QueryError
print(f"Database error: {e}")
except AppError as e:
# Catches all application errors
print(f"Application error: {e}")
except Exception as e:
# Catches everything else
print(f"Unexpected error: {e}")
Key points:
- Create a base exception for your application
- Group related exceptions in a hierarchy
- Catching a parent exception catches all child exceptions
- Makes error handling more organized
Quick Check: What happens when you catch a parent exception? (Answer: All child exceptions are also caught)
Best Practices
Writing Clean Custom Exceptions
# 1. Use meaningful names
# GOOD
class UserNotFoundError(Exception):
pass
# AVOID
class Error(Exception):
pass
# 2. Add docstrings
class ConfigurationError(Exception):
"""Raised when configuration is invalid or missing"""
pass
# 3. Include relevant context
class FileProcessingError(Exception):
def __init__(self, filename, operation, error):
self.filename = filename
self.operation = operation
self.error = error
super().__init__(f"Failed to {operation} {filename}: {error}")
# 4. Create a base exception for your application
class AppError(Exception):
"""Base exception for MyApp"""
pass
# 5. Use inheritance for organization
class ConfigError(AppError):
pass
class DataError(AppError):
pass
# 6. Don't overuse custom exceptions
# Only create them when they add value
class UserAlreadyExistsError(Exception):
"""Raised when trying to create a user that already exists"""
pass
Best practices summary:
- Use meaningful names — end with "Error"
- Add docstrings — document what the exception means
- Include context — add attributes for debugging
- Create a base exception — for your application
- Don't overuse — only create when they add value
Common Mistakes
Things to Watch Out For
Inheriting from the Wrong Class
# WRONG — inheriting from object
class MyError(object): # Not an exception!
pass
# CORRECT — inheriting from Exception
class MyError(Exception):
pass
Not Adding a Docstring
# AVOID — no documentation
class ConfigError(Exception):
pass
# GOOD — with docstring
class ConfigError(Exception):
"""Raised when configuration is invalid or missing"""
pass
Using Vague or Generic Names
# AVOID — too generic
class Error(Exception):
pass
# GOOD — specific and descriptive
class InvalidCredentialsError(Exception):
pass
class SessionExpiredError(Exception):
pass
Quick Check: What is the most common mistake with custom exceptions? (Answer: Inheriting from the wrong class or using vague names)
Try It Yourself
Experiment with user defined exceptions in the editor below. Modify the code and see what happens.
USER DEFINED EXCEPTION PRACTICE
========================================
1. BASIC CUSTOM EXCEPTION
Caught: Number must be positive, got -5
2. CUSTOM EXCEPTION WITH ATTRIBUTES
Error: Insufficient funds: balance 100, needed 150
Balance: 100, Needed: 150
3. EXCEPTION HIERARCHY
AgeError caught: Age cannot be negative: -5
User defined exception practice complete!
You've Got It!
You now understand user defined exceptions — how to create them, add custom attributes, raise them, handle them, and organize them in a hierarchy. This is a professional skill for writing robust Python applications!
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is a user defined exception?
Exception class. It represents application-specific errors.
Why should I create custom exceptions?
What class should a custom exception inherit from?
Exception class (or a subclass of Exception). This makes them behave like built-in exceptions.
Can I add custom data to a custom exception?
__init__ method and storing data as attributes. This is useful for debugging and error reporting.
What is the exception hierarchy?
What's a common interview question about custom exceptions?
Where to Go From Here
Now that you understand user defined exceptions, check out these related topics:
Logging Exception
Learn how to log exceptions for debugging and monitoring.
Learn More →Exception Assignments
Practice what you've learned with assignments.
Learn More →Error vs Exception
Review the difference between errors and exceptions.
Learn More →