Classes & OOP–Defining Your Own Exception Classes
When I started using Python, I was hesitant to write custom exception classes in my code. But defining your own error types can be of great value. You’ll make potential error cases stand out clearly, and as a result, your functions and modules will become more maintainable. You can also use custom error types to provide additional debugging information.
All of this will improve your Python code and make it easier to understand, easier to debug, and more maintainable. Defining your own exception classes is not that hard when you break it down to a few simple examples. Let’s say you. Wanted to validate an input string representing a person’s name in your application. A toy example for a name validator function might look like this:
In [1]: def validate(name):
...: if len(name)<10:
...: raise ValueError
If the validation fails, it throws a ValueError exception. That seems fitting and kind of Pythonic already. So far, so good.
However, there’s a downside to using a “high-level” generic exception class like ValueError. Imagine one of your teammates calls this function as part of a library and doesn’t know much about its internals. When a name fails to validate, it’ll look like this in the debug stack trace:
In [2]: validate('joe')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-2-84bf9236a789> in <module>
----> 1 validate('joe')
<ipython-input-1-e46a2bbae29e> in validate(name)
1 def validate(name):
2 if len(name)<10:
----> 3 raise ValueError
4
ValueError:
This stack trace isn’t really all that helpful. Sure, we know that something went wrong and that the problem had to do with an “incorrect value” of sorts. but to be able to fix the problem your temmate almost certainly has to look up the implementation of validate(). However, reading code costs time. And it can add up quickly.
Luckily we can do better. Let’s introduce a custom exception type to represent a failed name validation. We’ll base your new exception class on Python’s built-in ValueError. but make it speak for itself by giving it a more explicit name:
In [3]: class NameTooShortError(ValueError):
...: pass
...:
In [4]: def validate(name):
...: if len(name) < 10:
...: raise NameTooShortError(name)
Now we have a “self-documenting” NameTooShortError exception type that extends the built-in ValueError class. Generally, you’ll want to either derive your custom exceptions from the root Exception class or the other built-in Python exceptions like ValueError or TypeError–whichever feels appropriate.
Also, see how we’re now passing the nam variable to the constructor of our custom exception class when we instantiate it inside validate?The new implementation results in a much nicer stack trace for your colleague:
In [5]: validate('jane')
---------------------------------------------------------------------------
NameTooShortError Traceback (most recent call last)
<ipython-input-5-e12fe3239433> in <module>
----> 1 validate('jane')
<ipython-input-4-fa64b43535ec> in validate(name)
1 def validate(name):
2 if len(name) < 10:
----> 3 raise NameTooShortError(name)
4
NameTooShortError: jane
Once again, try to put yourself in your teammate’s shoes. Custom exception classes make it much easier to understand what’s going on when things go wrong (and eventually they always do).
The same is true even if you’re working on a code base all by yourself. A few weeks or months down the road you’ll have a much easier time maintaining your code if it’s well-structured.
By spending just 30 seconds on defining as simple exception class, this code snipped became much more communicative already. But let’s keep going. There’s more to cover.
Whenever you’re publicly releasing a Python package, or even if you’re creating a reusable module for your company, it’s good practice to create a custom exception base class for the module and athen derive all of your other exceptions from it.
Here’s how to create a custom exception hierarchy for all exceptions in a module or package. The first step is to declare a base class that all of our concrete errors will inherit from:
In [6]: class BaseValidationError(ValueError):
...: pass
Now, all of our “real” error classes can be derived from the base error class. This gives a nice and clean exception hierarchy with little extra effort:
In [6]: class BaseValidationError(ValueError):
...: pass
...:
In [7]: class NameTOOShortError(BaseValidationError):
...: pass
...:
In [8]: class NameTOOLongError(BaseValidationError):
...: pass
...:
In [9]: class NameTOOCuteError(BaseValidationError):
...: pass
For example, this allows users of your package to write try…except
try:
validate(name)
except BaseValidationError as err:
handle_validation_error(err)
People can still catch more specific exceptions that way, but if they don’t want to, at least they won’t have to resort to snapping up all exceptions with a catchall except statement. This is generally considered an anti-pattern–it can silently swallow and hide unrealated errors and make your programs much harder to debug.
Of course you can take this idea further and logically group your exceptions into fine grained sub-hierarchies. But be careful–it’s easy to introduce unnecessary complexity by going overboard with this.
In conclusion, defining custom exception classes makes it easier for your users to adopt an it’s easier to ask for forgiveness than permission (EAFP)