Creating Custom Exception Classes in Python
Course: Python Training
Topic: Creating Custom Exception Classes
Course Link: Python Training Course at JustAcademy
Demo Link: Register for a Course Demo
Website: Visit JustAcademy
1. Introduction to Custom Exception Classes
Python provides built-in exceptions such as ValueError, TypeError, ZeroDivisionError, and FileNotFoundError to report common programming errors. However, real-world applications often have specific rules that cannot be described clearly using only built-in exceptions.
For example, an e-commerce application may need to report an invalid order, a banking application may need to report insufficient funds, and a student management system may need to report an invalid student record. In these situations, developers can create custom exception classes that represent application-specific errors.
A custom exception is a class created by the programmer to represent a particular error condition. It can be raised with the raise statement and handled using try and except.
2. Learning Objectives
After completing this topic, learners will be able to:
- Understand the purpose of custom exceptions.
- Create exception classes using Python class syntax.
- Inherit custom exceptions from the built-in
Exception class.
- Raise custom exceptions using the
raise statement.
- Catch custom exceptions with specific
except blocks.
- Create a common base exception for an application.
- Pass useful messages and additional information to exceptions.
- Use custom exceptions in practical business applications.
- Apply best practices for exception design and handling.
3. What Is a Custom Exception?
A custom exception is a user-defined exception class that represents an error specific to an application or business requirement.
Python allows developers to define custom exceptions by inheriting from Exception or from an appropriate subclass of Exception. Custom exception classes are generally kept simple and use names ending in Error.
Example of a Custom Exception
class InvalidAgeError(Exception):
pass
raise InvalidAgeError("Age must be at least 18.")
Explanation:
class InvalidAgeError(Exception): defines a new exception class.
Exception is the parent class.
pass indicates that no additional class implementation is required.
raise creates and raises an instance of the custom exception.
- The message explains why the operation failed.
The example raises an exception that is intentionally not caught, so Python displays a traceback ending with a message similar to:
InvalidAgeError: Age must be at least 18.
In practical applications, you can catch the exception when the program needs to recover or display a user-friendly message.
4. Why Do We Need Custom Exceptions?
Built-in exceptions are useful, but they may not describe the exact meaning of a business problem. Custom exceptions allow developers to communicate these problems more clearly.
| Situation |
Possible Custom Exception |
Purpose |
| Bank account has insufficient funds. |
InsufficientBalanceError |
Identifies a withdrawal that exceeds the balance. |
| Product stock is unavailable. |
OutOfStockError |
Identifies an order that cannot be fulfilled. |
| Student marks are outside the accepted range. |
InvalidMarksError |
Identifies invalid academic input. |
| User registration data violates application rules. |
RegistrationError |
Represents a registration-related failure. |
| Payment processing fails. |
PaymentProcessingError |
Represents a payment workflow failure. |
| A requested application record does not exist. |
RecordNotFoundError |
Identifies a missing business record. |
Key Benefits
- Clearer error messages: Exception names describe the problem more precisely.
- Better error handling: Different problems can be handled using separate
except blocks.
- Improved maintainability: Developers can understand application-specific failure conditions more easily.
- Reusable error definitions: Multiple functions can raise the same custom exception.
- Consistent application behavior: Related modules can follow a common exception structure.
5. Creating a Custom Exception Class
The simplest way to create a custom exception is to define a class that inherits from Exception.
Syntax
class CustomError(Exception):
"""Description of the error."""
pass
Here, CustomError is the new exception class, and Exception is its parent class.
Example: Invalid Marks
class InvalidMarksError(Exception):
"""Raised when marks are outside the valid range."""
def validate_marks(marks):
if not 0 <= marks <= 100:
raise InvalidMarksError("Marks must be between 0 and 100.")
return marks
try:
print(validate_marks(85))
except InvalidMarksError as error:
print("Validation failed:", error)
Output:
85
When validate_marks(120) is called, the function raises InvalidMarksError, and the matching exception handler displays:
Validation failed: Marks must be between 0 and 100.
6. Inheriting from Exception
Inheritance allows a custom exception to receive the standard exception behavior provided by Python. By inheriting from Exception, a custom class can be raised, caught, and used with standard exception-handling mechanisms.
class ApplicationError(Exception):
"""Base exception for application-specific errors."""
class ConfigurationError(ApplicationError):
"""Raised when application configuration is invalid."""
class AuthenticationError(ApplicationError):
"""Raised when authentication fails."""
In this example, ApplicationError is the base exception for the application. The other two exception classes inherit from it.
Because both child classes inherit from ApplicationError, a handler for the base class can catch either exception.
try:
raise AuthenticationError("Invalid credentials.")
except ApplicationError as error:
print("Application problem:", error)
Output:
Application problem: Invalid credentials.
Important: Custom application exceptions should normally inherit from Exception, directly or indirectly, rather than from BaseException. The latter is intended to include system-exiting and interruption-related exceptions that ordinary application handlers should not generally intercept.
7. Raising Custom Exceptions with raise
Defining a custom exception class does not raise it automatically. You must use the raise statement when the relevant error condition occurs.
Example: Checking a Minimum Account Balance
class InsufficientBalanceError(Exception):
"""Raised when an account lacks sufficient funds."""
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("Withdrawal amount must be positive.")
if amount > balance:
raise InsufficientBalanceError(
"Withdrawal amount exceeds the available balance."
)
return balance - amount
try:
remaining_balance = withdraw(5000, 7000)
print("Remaining balance:", remaining_balance)
except InsufficientBalanceError as error:
print("Transaction failed:", error)
Output:
Transaction failed: Withdrawal amount exceeds the available balance.
The function uses a built-in ValueError for an invalid withdrawal amount and a custom InsufficientBalanceError for a business-specific condition.
8. Handling Custom Exceptions with try-except
Custom exceptions are handled in the same way as built-in exceptions. A matching except block receives the exception object and can inspect its message.
Example
class OutOfStockError(Exception):
"""Raised when the requested product quantity is unavailable."""
def purchase_product(stock, quantity):
if quantity <= 0:
raise ValueError("Quantity must be positive.")
if quantity > stock:
raise OutOfStockError("Requested quantity is unavailable.")
return stock - quantity
try:
remaining_stock = purchase_product(10, 15)
print("Remaining stock:", remaining_stock)
except OutOfStockError as error:
print("Purchase failed:", error)
except ValueError as error:
print("Invalid quantity:", error)
Output:
Purchase failed: Requested quantity is unavailable.
How It Works
- The function receives the available stock and requested quantity.
- It checks whether the quantity is valid.
- If the requested quantity exceeds stock, it raises
OutOfStockError.
- Python skips the remaining statements in the
try block.
- The matching
except block handles the custom exception.
- The application displays a meaningful message instead of continuing the invalid purchase.
9. Creating a Custom Exception with a Message
A custom exception can use the standard constructor inherited from Exception. This means you can pass a message when creating an instance without defining an __init__() method.
class InvalidProductError(Exception):
pass
error = InvalidProductError("The selected product is invalid.")
print(error)
Output:
The selected product is invalid.
The message is stored as part of the exception's arguments and is available through its string representation.
10. Adding Custom Attributes to an Exception
Sometimes a message alone is not enough. You may want the exception to carry structured information, such as an account number, the requested amount, an error code, or the available balance.
To do this, define an __init__() method and store the required information as instance attributes.
Example: Insufficient Balance with Details
class InsufficientBalanceError(Exception):
def __init__(self, balance, requested_amount):
self.balance = balance
self.requested_amount = requested_amount
message = (
f"Available balance is {balance}, "
f"but {requested_amount} was requested."
)
super().__init__(message)
try:
raise InsufficientBalanceError(2000, 3500)
except InsufficientBalanceError as error:
print(error)
print("Available balance:", error.balance)
print("Requested amount:", error.requested_amount)
Output:
Available balance is 2000, but 3500 was requested.
Available balance: 2000
Requested amount: 3500
Understanding the Code
__init__() initializes the custom exception object.
self.balance stores the available balance.
self.requested_amount stores the requested amount.
super().__init__(message) initializes the standard exception with the message.
- The handler can access both the message and the structured attributes.
Custom attributes are useful when the caller needs to make a decision based on the error details rather than merely displaying a message.
11. Creating a Base Exception and Specialized Exceptions
In larger applications, it is useful to create a shared base exception and derive more specific exceptions from it. This allows the application to handle individual errors separately or catch all errors belonging to a particular module.
Example: E-Commerce Exceptions
class ShopError(Exception):
"""Base exception for shopping operations."""
class ProductNotFoundError(ShopError):
"""Raised when a product cannot be found."""
class OutOfStockError(ShopError):
"""Raised when a product has insufficient stock."""
class InvalidOrderError(ShopError):
"""Raised when an order contains invalid data."""
def place_order(product_exists, stock, quantity):
if not product_exists:
raise ProductNotFoundError("The product does not exist.")
if quantity <= 0:
raise InvalidOrderError("Order quantity must be positive.")
if quantity > stock:
raise OutOfStockError("Not enough stock for this order.")
return "Order placed successfully."
try:
print(place_order(True, 5, 8))
except ProductNotFoundError as error:
print("Product error:", error)
except OutOfStockError as error:
print("Stock error:", error)
except InvalidOrderError as error:
print("Order error:", error)
except ShopError as error:
print("General shopping error:", error)
Output:
Stock error: Not enough stock for this order.
The base class ShopError groups the shopping-related exceptions. Each specialized class describes a particular problem.
12. Catching a Base Exception Class
Because custom exceptions inherit from their parent classes, a handler for a parent class can catch exceptions raised by any of its subclasses.
class PaymentError(Exception):
pass
class CardDeclinedError(PaymentError):
pass
class PaymentGatewayError(PaymentError):
pass
try:
raise CardDeclinedError("The card was declined.")
except PaymentError as error:
print("Payment could not be completed:", error)
Output:
Payment could not be completed: The card was declined.
This approach is useful when different payment failures require a common response. If the application needs different actions for individual failure types, use specific handlers before the general base-class handler.
13. Custom Exceptions and the finally Block
The finally block is used for cleanup operations that should run whether an exception occurs or not. A custom exception can be raised inside a try block while finally performs cleanup.
class InvalidTransactionError(Exception):
pass
try:
print("Starting transaction.")
raise InvalidTransactionError("Transaction data is invalid.")
except InvalidTransactionError as error:
print("Transaction failed:", error)
finally:
print("Transaction processing finished.")
Output:
Starting transaction.
Transaction failed: Transaction data is invalid.
Transaction processing finished.
In real applications, the finally block may release a resource, close a connection, or perform other necessary cleanup. Avoid placing a return statement in finally when it could accidentally override a return value or suppress an active exception.
14. Exception Chaining with Custom Exceptions
Exception chaining is useful when a low-level exception needs to be translated into a higher-level application exception while preserving the original cause.
Python supports explicit exception chaining with raise NewException(...) from original_error.
Example: Configuration Loading
class ConfigurationError(Exception):
"""Raised when configuration cannot be loaded."""
def parse_port(value):
try:
port = int(value)
except ValueError as error:
raise ConfigurationError(
"The configured port must be an integer."
) from error
if not 1 <= port <= 65535:
raise ConfigurationError(
"The configured port must be between 1 and 65535."
)
return port
try:
print(parse_port("invalid"))
except ConfigurationError as error:
print("Configuration problem:", error)
Output:
Configuration problem: The configured port must be an integer.
The original conversion error remains available as the cause. This helps developers investigate the underlying failure while the application receives an error message appropriate to the configuration task.
15. Difference Between Built-in and Custom Exceptions
| Feature |
Built-in Exception |
Custom Exception |
| Definition |
Provided by Python. |
Defined by the application developer. |
| Purpose |
Represents common programming and runtime problems. |
Represents application-specific or business-related failures. |
| Examples |
ValueError, TypeError, IndexError |
InvalidOrderError, PaymentError |
| Creation |
Already available in Python. |
Created by defining a class that inherits from Exception or a suitable subclass. |
| Handling |
Handled using matching except blocks. |
Handled using matching except blocks or a custom base exception. |
| Best use |
General errors such as invalid values or incorrect types. |
Domain-specific errors requiring clear application meaning. |
16. Best Practices for Designing Custom Exceptions
- Use meaningful names: Names such as
InvalidOrderError and PaymentProcessingError clearly describe the failure.
- Inherit from Exception: Use
Exception or an appropriate subclass instead of inheriting directly from BaseException.
- Keep classes simple: A custom exception often needs only a clear name and message.
- Use a base exception where useful: Group related application exceptions under a shared parent class.
- Store structured information only when needed: Add attributes when the caller needs specific data about the failure.
- Call the parent initializer: When implementing a custom
__init__(), use super().__init__(message) to initialize the standard exception message.
- Catch specific exceptions first: Put specialized handlers before broader handlers.
- Avoid unnecessary custom classes: Use built-in exceptions when they already describe the problem accurately.
- Preserve useful context: Use exception chaining when translating an underlying error.
- Avoid sensitive data: Do not include passwords, secret keys, or other sensitive information in exception messages.
- Document expected failures: Explain which exceptions a function may raise and under what conditions.
- Test exception behavior: Verify that invalid inputs raise the expected exception and valid inputs work correctly.
17. Common Mistakes When Creating Custom Exceptions
| Mistake |
Why It Is a Problem |
Recommended Approach |
| Not inheriting from an exception class |
The class cannot be raised as a normal Python exception. |
Inherit from Exception or a suitable subclass. |
| Using unclear class names |
Developers cannot easily understand the error's purpose. |
Use descriptive names ending in Error where appropriate. |
| Creating a custom exception for every minor condition |
The exception hierarchy becomes unnecessarily complicated. |
Reuse built-in exceptions when they adequately describe the issue. |
Forgetting super().__init__() in a custom initializer |
The standard exception message and argument handling may not behave as expected. |
Pass the message to the parent initializer. |
| Catching only the base exception when different handling is required |
Specific failures may receive the same response unintentionally. |
Use specific handlers before the base-class handler. |
| Ignoring exceptions |
Failures may go unnoticed, making debugging difficult. |
Handle, log, or propagate the exception appropriately. |
| Using exceptions as ordinary branching logic |
The code may become harder to read and maintain. |
Use normal conditions for routine decisions and exceptions for invalid or exceptional situations. |
18. Practice Exercises
Exercise 1: Create an Invalid Age Exception
Create a class called InvalidAgeError. Write a function that raises this exception if the supplied age is less than 18.
Exercise 2: Create a Product Stock Exception
Create an OutOfStockError class. Write a function that checks available stock before accepting a product order.
Exercise 3: Create a Custom Exception with Attributes
Define an InsufficientBalanceError class that stores the available balance and requested withdrawal amount as attributes.
Exercise 4: Create an Exception Hierarchy
Define a base exception called LibraryError and two subclasses named BookNotFoundError and BookUnavailableError. Demonstrate catching both subclasses through the base exception.
Exercise 5: Use Exception Chaining
Write a function that converts a text value into an integer. If conversion fails, raise a custom InvalidConfigurationError using raise ... from error.
19. Mini Project: Student Management System
Objective: Build a small student management system that uses custom exceptions to validate student records.
Project Requirements
- Create a base exception called
StudentManagementError.
- Create
InvalidStudentNameError for an empty student name.
- Create
InvalidMarksError for marks outside the range of 0 to 100.
- Create a function that validates the student's name and marks.
- Catch validation errors and display clear messages.
- Allow valid student records to be processed normally.
Sample Solution
class StudentManagementError(Exception):
"""Base exception for student management errors."""
class InvalidStudentNameError(StudentManagementError):
"""Raised when the student name is invalid."""
class InvalidMarksError(StudentManagementError):
"""Raised when marks are outside the accepted range."""
def register_student(name, marks):
if not isinstance(name, str) or not name.strip():
raise InvalidStudentNameError(
"Student name cannot be empty."
)
if not isinstance(marks, (int, float)) or isinstance(marks, bool):
raise InvalidMarksError(
"Marks must be a numeric value."
)
if not 0 <= marks <= 100:
raise InvalidMarksError(
"Marks must be between 0 and 100."
)
return {
"name": name.strip(),
"marks": marks
}
try:
student = register_student("Anita", 88)
print("Student registered:", student)
except StudentManagementError as error:
print("Registration failed:", error)
Output:
Student registered: {'name': 'Anita', 'marks': 88}
Expected Test Cases
| Input |
Expected Result |
Name: Anita, marks: 88 |
Student record is accepted. |
Name: empty string, marks: 75 |
InvalidStudentNameError is raised. |
Name: Ravi, marks: 120 |
InvalidMarksError is raised. |
Name: Ravi, marks: "excellent" |
InvalidMarksError is raised. |
20. Interview Questions and Answers
Q1. What is a custom exception in Python?
A custom exception is a user-defined exception class that represents an application-specific error condition.
Q2. How do you create a custom exception class?
Define a class that inherits from Exception or an appropriate subclass, for example: class InvalidAgeError(Exception): pass.
Q3. Why should custom exceptions inherit from Exception?
Inheriting from Exception allows the class to work with Python's standard exception-handling system and separates ordinary application errors from system-exiting exceptions.
Q4. How do you raise a custom exception?
Use the raise statement, such as raise InvalidAgeError("Age is invalid.").
Q5. Can a custom exception have its own attributes?
Yes. A custom exception can define an __init__() method to store attributes such as an error code, requested amount, or invalid value.
Q6. Why should super().__init__() be used in a custom exception initializer?
It initializes the parent exception with the supplied message and preserves standard exception argument behavior.
Q7. What is a custom exception hierarchy?
It is a group of related exception classes organized through inheritance, usually with a shared application-specific base exception.
Q8. Can one except block handle multiple custom exceptions?
Yes. It can catch a common parent class or list multiple exception classes in a tuple when they need the same handling.
except (InvalidAgeError, InvalidMarksError) as error:
print("Validation failed:", error)
Q9. What is the advantage of using custom exceptions instead of generic exceptions?
Custom exceptions communicate application-specific failures more clearly and let callers handle different error categories precisely.
Q10. When should you avoid creating a custom exception?
Avoid creating one when a built-in exception already describes the problem clearly or when the new class adds unnecessary complexity without improving error handling.
21. Quick Revision Checklist
- A custom exception is defined by the programmer.
- Custom exceptions normally inherit from
Exception or one of its subclasses.
- Use
raise to trigger the custom exception.
- Use
try and except to handle the exception.
- Use descriptive names, usually ending in
Error.
- Use
super().__init__(message) when defining a custom initializer.
- Store extra attributes only when structured error information is useful.
- Use a common base exception to group related application errors.
- Use exception chaining to preserve the cause of a translated error.
- Test both valid and invalid cases.
22. Conclusion
Creating custom exception classes is an important Python programming skill for building reliable and maintainable applications. Custom exceptions allow developers to represent business-specific failures, provide clear error messages, and design structured error-handling strategies.
By combining inheritance, the raise statement, try/except, custom attributes, and exception chaining, developers can make their applications easier to debug and safer to maintain.
23. Additional Learning Resources
Read the official Python documentation on errors and user-defined exceptions for further details about exception classes and exception handling.
Explore the Python Training Course at JustAcademy, register for a course demo, or visit the JustAcademy homepage for more learning opportunities.