Topics: Custom exception classes

Errors and Exceptions

First of all, what is an exception? As outlined in the Python docs there are two big kinds of errors: syntax errors and exceptions.

Syntax Error:
A syntax error is an error that makes it impossible to parse a program. That’s why it’s also called a parsing error. Whenever we made a mistake in the syntax of our code (for example, we might forgot a ‘:’ somewhere) the parser will complain with a Syntax Error: invalid syntax, pointing us to the offending line.

Exception:
An exception is an error that is detected while a program is being executed. Python has several built-in exceptions like ValueError or TypeError. However, when creating a module, it can often be useful to create custom exception classes that are more specific than the generic exceptions built into Python.

How to define a custom exception class

As mentioned in the Python docs, exceptions should be derived from the Exception class. Either directly as in:

class MySpecificError(Exception):
  pass 

or indirectly by subclassing a more specific exception like ValueError:

class MySpecificError(ValueError):
  pass 

Custom exception classes can be very simple (like the ones above) or complex, providing additional details and containing methods. If you want to see how custom exception classes are used in larger projects, take a look at the Open AI gym exception classes or the Python click package.

When are custom exception classes useful?

Most of the time, the standard Python exception classes (like TypeError, ValueError etc.) are sufficient, especially when customizing them with a specific message. Consider the self.birthyear attribute of our CastleKilmereMember class. The birthyear is used for computing the current age of a Castle Kilmere member. So if birthyear has the wrong type, that is, it’s not an integer like 1991, we won’t get a valid age. So we should introduce a type check on the birthyear in our __init__() constructor:

if type(birthyear) is not int:
    raise TypeError("The birthyear is not a valid integer. Try something like 1991")

However, when defining a module we might have several distinct errors in that module. In such a case it can be useful to create custom exception classes. In the example above we might throw a specific error like InvalidBirthyearError. When creating custom exception classes it’s common practice to define a base exception class for the module, and subclass specific exception classes for the different error conditions. We will create a file called error.py to define our exception classes and use them in our code.

class BaseError(Exception):
    """Base class for exceptions in this module"""
    pass

class InvalidBirthyearError(BaseError):
    """Raised when the birthyear argument is not a valid integer like 1991"""
    pass

With this custom exception we could change our type check:

import error
...

if type(birthyear) is not int:
    raise error.InvalidBirthyearError("The birthyear is not a valid integer. Try something like 1991")

Potential benefits of custom exception classes

Custom exception classes can have several benefits:

  • When something goes wrong, error messages will be more specific and readable
  • Anyone using the code (teammates, clients, etc.) can be sure to catch all exceptions our module may raise if they catch BaseException
  • If needed, more specific exceptions can be catched, giving problem-specific details
  • We (or the maintainers of the module) can add and modify existing exceptions without breaking existing client code

Creating our own exception classes

There are other places of our Magical Universe in which custom exception classes might offer more clarity. Consider the first part of our exhibits_trait() function

    def exhibits_trait(self, trait: str) -> bool:
        try:
            value = self._traits[trait]
            return value
        except KeyError as e:
            print(f"{self._name} does not have a character trait with the name {e}")
            return False

If a person does not posses a trait, a KeyError will be raised which is catched and handled by our except block. Instead of just printing that a Castle Kilmere member does not possess a specific trait, we could also raise a TraitDoesNotExistError. First, we have to define this exception class in our error.py file:

class BaseError(Exception):
    """Base class for exceptions in this module"""
    pass

class InvalidBirthyearError(BaseError):
    """Raised when the birthyear argument is not a valid integer like 1991"""
    pass

class TraitDoesNotExistError(BaseError):
    pass

Now we can use the exception to tell the user precisely what happened and what he can do about it:

    def exhibits_trait(self, trait: str) -> bool:
        try:
            value = self._traits[trait]
            return value
        except KeyError as e:
            raise error.TraitDoesNotExistError(f"{self._name} does not possess a character trait with the name '{trait}'. Use the add_trait() function to add traits.")

Keep in mind that these examples are not supposed to mean that we have to use custom exception classes in our code. You should stick to the built-in Python exceptions whenever they are sufficient. Use custom exception classes only when they are needed.