Particularly when creating library packages in Python, raising exceptions is a great way to let the downstream developer know about problems occuring while executing code from within the library. Python’s built-in exceptions cover a whole host of cases. However, some problems might be library-specific and deserve a custom exception.
Custom Exceptions
Creating a custom exception in Python isn’t particularly hard when observing a few rules:
- Custom exceptions shall be derived from Python’s Exception class
- By convention, exception names shall end with “Error”
1class MyLibError(Exception):
2 pass
In many library packages, you will find exceptions similar to the one shown in the code example above. There’s nothing wrong with that; if chosen wisely, the exception name by itself will already tell what went wrong. However, the more details an exception provides about the circumstances it was raised in, the easier to diagnose and debug the problem, and the more appropriate the reaction to an exception.
Tell Me Why
In my projects, I like to pass messages to an exception raised, adding some descriptive information, maybe even disclose the offending element. Let’s illustrate this with an example:
1class ValueOutOfScopeError(Exception):
2
3 def __init__(self, message: str)
4 this.message = message
5
6 def __repr__(self):
7 return this.message
8
9 def __str__(self):
10 return this.message
11
12
13def do_something(value: int):
14 """Do something with a value between 5 and 10"""
15 if (value < 5) or (value > 10):
16 raise ValueOutOfScopeError("{} is not between 5 and 10".format(value))
As you can see, the exception clearly tells what’s wrong — it indicates the offending value, and also states what your code would have expected instead. Now, we could improve the example by defining a default message for our custom exception — there might be situations where typing a lengthy message does just not add value, and a brief default would do. However, doing this within the code structure shown above would create quite bloaty code.
Keeping Things Lean
In bigger projects with more than just a few custom exceptions, I therefore use my own Exception base class. Since it’s good practice to document custom exceptions (or any custom class or function) with Python Documentation Strings, I use these documentation strings to define a default message:
1from typing import Optional
2
3class MyLibBaseException(Exception):
4
5 def __init__(self, message: Optional[str] = None):
6 super().__init__()
7 if (message is None) or (message == ""):
8 self.message = self.__doc__
9 else:
10 self.message = str(message)
11
12 def __str__(self):
13 return self.message
14
15 def __repr__(self):
16 return self.message
17
18
19class MyLibValueOutOfScopeError(MyLibBaseException):
20 """MyLib Error: value is outside of the expected scope"""
21
22
23class MyLibValueTooBigError(MyLibValueOutOfScopeError):
24 """MyLib Error: value is bigger than expected"""
25
26
27class MyLibValueTooLowError(MyLibValueOutOfScopeError):
28 """MyLibError: value is lower than expected"""
With Extra Cheese, Please
With this custom base exception in place, defining additional custom exceptions with a default message is simple, elegant, and easy to read — just create an additional class with a documentation string. The base exception class takes care of the rest. As shown in the example below, these custom exceptions can be raised in various ways:
1import sys
2
3
4def do_something(value: int):
5 if value < 0:
6 raise MyLibValueTooLowError
7 elif value > 10:
8 raise MyLibValueTooBigError("Too big!")
9
10 daList = [1, 2, 3, 4, 5]
11 try:
12 print(daList[value])
13 except IndexError as e:
14 raise MyLibValueOutOfScopeError(e)
15
16
17if __name__ == '__main__':
18 try:
19 do_something(int(sys.argv[1]))
20 except MyLibValueOutOfScopeError as e:
21 print(e)
Disclaimer: yes, this example is cheesy. But it nicely illustrates the three main use cases with our custom exception:
- raise them without arguments — will produce the default error message
- raise them with a custom message — will produce the custom error message
- raise them from another exception — will pass on the originating exception’s message
Final Thoughts
Custom exceptions are a must-have in nearly any bigger Python project. In my opinion, combining documentation strings and error messages is an elegant approach to communicating failure verbosely, without unnecessarily bloating my code. Is this the most pythonic way to deal with custom exceptions? Honestly, I don’t know. For me it’s pythonic enough, whatever this means. It works, and it doesn’t feel wrong to do it this way.