Day 3: Attaching Custom Exceptions to Functions and Classes in Python
Tags: 100daysofcode, python, django
Published: 2020-04-20 05:55:29

Having too many custom exceptions on a project can be a pain, but a few choices are nice. The problem is that in complex libraries having to import both functions and exceptions becomes a drag. To mitigate having to remember to import custom exceptions, this is a handy pattern you can use in a project and can be done on both functions and classes.

## Attaching a Custom Exception to Functions

This works because Python functions are first-class objects. They can be passed around as things, and in this case, have things assigned to them.

``` python
# logic.py
class DoesNotCompute(Exception):
    """ Easy to understand naming conventions work best! """
    pass

def this_function(x):
    """ This function only works on numbers."""
    try:
        return x ** x
    except TypeError:
        raise DoesNotCompute("Function does not compute")

# Assign DoesNotCompute exception to this_function
this_function.DoesNotCompute = DoesNotCompute
```

Now I can import the function, and it won't just throw `DoesNotCompute` exceptions, it will also carry the function along with the import:

``` python
>>> from logic import this_function
>>> this_function(5)
3125
>>> this_function(4.5)
869.8739233809259
>>> this_function('will throw an error.')
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "logic.py", line 10, in this_function
    raise DoesNotCompute
DoesNotCompute
```

Alright, that doesn't seem like much, but let's add in some exception handling:

``` python
>>> try:
...     this_function('is an example')
... except this_function.DoesNotCompute:
...     print('See what attaching custom exceptions to functions can do?')
...
...
See what attaching custom exceptions to functions can do?
```

## Attaching the Custom Exception to Classes

All we have to do is enhance our existing `logic.py` file by adding `ThisClass`:

``` python
# logic.py
class DoesNotCompute(Exception):
    """ Easy to understand naming conventions work best! """
    pass

# removed the function example for clarity

class ThisClass:
    # Assign DoesNotCompute to this class
    DoesNotCompute = DoesNotCompute

    def this_method(self, x):
        """ This method only works on numbers."""
        try:
            return x ** x
        except TypeError:
            raise self.DoesNotCompute("Class does not compute")
```

Now to demonstrate in the shell (Python REPL for the semantic purists):

``` python
>>> from logic import ThisClass
>>> this_class = ThisClass()
>>> this_class.this_method(3.3)
51.415729444066585
>>> this_class.this_method("Don't create too many custom exceptions")
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "logic.py", line 24, in this_method
    raise DoesNotCompute
DoesNotCompute
>>> try:
...     this_class.this_method('I like ice cream')
... except ThisClass.DoesNotCompute:
...     print('Milk, ice, and fruit in a blender is nice too')
...
...
Milk, ice, and fruit in a blender is nice too
```

# Admonition: Don't Go Crazy

Rather than use this trick all over the place, considering using it in a few strategic places to powerful effect. For example, Django uses it only in a few places, and publicly only on `MyModelClass.DoesNotExist` and `MyModelClass.MultipleObjectsReturned`. By limiting Django's use of this technique, Django libraries are that much easier to comprehend. In this case, less complexity means more.

I say this because this pattern lends itself to creating custom exceptions to the point of effectively replacing Python's stock exceptions with your own. This makes for harder-to-maintain and harder-to-learn projects.

Not that I've ever done that. Ahem.

- Updated 2020/04/20: Added missing `self` in `ThisClass` thanks to **Chiheb Nexus Lad**.