Python Custom Exceptions: Build Your Own Error Types
Imagine your car's dashboard had only one warning light that just said "Problem." Not very helpful, right? You'd want to know: is the engine overheating? Is the oil low? Is a door open? The more specific the warning, the faster you can fix the issue.
Python's built-in exceptions like ValueError and TypeError are like generic warning lights. They tell you something is wrong, but they don't speak the language of your application. When you're building a banking app, wouldn't it be more useful to see InsufficientFundsError than a plain ValueError?
In this tutorial, you'll learn to create your own exception types that describe exactly what went wrong in your specific application. This is how professional Python developers write clean, maintainable error handling.
Why Create Custom Exceptions?
Let's look at a function that uses only built-in exceptions:
The problem here is that both errors are ValueError. The calling code can't easily tell the difference between "invalid amount" and "insufficient funds." It has to read the error message string, which is fragile and ugly.
Custom exceptions solve this by giving each error its own type. The calling code can catch exactly the error it cares about:
Now the caller can handle each situation differently. Maybe insufficient funds shows a "deposit more" button, while an invalid amount shows a "please enter a positive number" message. Custom exceptions make this easy.
How to Create a Basic Custom Exception
A custom exception is just a class that inherits from Exception (or one of its subclasses). The simplest version needs only two lines:
The pass keyword means "this class has no extra code." It inherits everything it needs from Exception — the ability to store a message, be raised, and be caught.
By convention, custom exception class names end with Error. This makes them instantly recognizable in code.
Let's see a practical example. Here's a password validator that uses a custom exception:
How to Add Data to Custom Exceptions
The real power of custom exceptions comes from adding your own attributes. Instead of just a message string, you can attach any data the caller needs to handle the error properly.
You do this by adding an __init__ method to your exception class. Just remember to call super().__init__() to keep the normal exception behavior working.
Now the caller doesn't have to parse a string to figure out the numbers. The balance, amount, and deficit are all available as clean attributes on the exception object.
Here's another example with a validation error that tracks which field failed:
Building Exception Hierarchies
When your application has many custom exceptions, you can organize them into a hierarchy using inheritance. This lets callers choose how specific their error handling should be.
Think of it like a filing system. You might have a general "Application Error" folder, with sub-folders for "Payment Error" and "User Error," and even more specific folders inside those.
Notice that except PaymentError catches both InsufficientFundsError and CardDeclinedError because they both inherit from PaymentError. This is the power of hierarchies — you can catch broadly or narrowly, depending on what makes sense.
You could also catch at the very top with except AppError to handle any error from your application, while still letting system errors (like MemoryError) pass through.
Here's a diagram showing the hierarchy as a tree. Each level inherits from the one above:
An InsufficientFundsError is also a PaymentError, also an AppError, and also an Exception. This chain of "is-a" relationships is what makes hierarchies so powerful for error handling.
Real-World Custom Exception Patterns
Let's put it all together with a more realistic example. Here's a simple user registration system with a clean exception hierarchy:
This pattern gives you three levels of control. You can catch UsernameError or EmailError for specific handling. Or catch RegistrationError to handle all registration problems the same way.
Another useful pattern is creating exceptions that suggest a fix:
Practice Exercises
Create a custom exception class called NegativeAgeError that inherits from Exception.
Then write a function called set_age(age) that raises NegativeAgeError with the message "Age cannot be negative: X" (where X is the age) if the age is negative. Otherwise, it prints "Age set to X".
Test it with set_age(25) and catch the error from set_age(-5).
Read the code below and predict what it will print.
class AppError(Exception):
pass
class DatabaseError(AppError):
pass
class ConnectionError(DatabaseError):
pass
errors = [ConnectionError("timeout"), DatabaseError("corrupt"), AppError("unknown")]
for e in errors:
try:
raise e
except ConnectionError:
print("connection")
except DatabaseError:
print("database")
except AppError:
print("app")Create a custom exception called TemperatureError with:
__init__ that accepts temp and unit parametersself.temp and self.unit"Invalid temperature: X Y" to super().__init__() (where X is temp and Y is unit)Then write a function check_water_temp(temp) that:
TemperatureError(temp, "C") with message if temp < 0 or temp > 100"Water is liquid at X C" otherwiseTest with check_water_temp(50), then catch errors from check_water_temp(-10) and print both the error message AND the temp attribute.
Create an exception hierarchy for a shopping cart:
1. CartError(Exception) - base class (just pass)
2. EmptyCartError(CartError) - just pass
3. ItemNotFoundError(CartError) - accepts item_name in __init__, stores it as self.item_name, message: "Item not found: X"
Then write a function checkout(cart) that:
EmptyCartError("Cart is empty") if the cart list is empty"Checking out N items" where N is the number of itemsAnd a function remove_item(cart, item) that:
ItemNotFoundError(item) if the item is not in the cart"Removed: X"Test:
1. Call checkout([]) and catch EmptyCartError
2. Call remove_item(["apple", "bread"], "milk") and catch ItemNotFoundError, printing the item_name attribute
3. Call checkout(["apple", "bread"]) (success)
The code below uses generic ValueError for all errors. Refactor it to use custom exceptions.
Create:
GradeError(Exception) - base class, just passInvalidGradeError(GradeError) - accepts grade in __init__, stores as self.grade, message: "Invalid grade: X"InvalidSubjectError(GradeError) - accepts subject in __init__, stores as self.subject, message: "Unknown subject: X"Replace the ValueError raises with the appropriate custom exceptions. Update the except blocks to catch InvalidGradeError and InvalidSubjectError separately.
The output should remain exactly the same.