DEV Community

Cover image for Day 3: Attaching Custom Exceptions to Functions and Classes in Python
Daniel Feldroy for Feldroy

Posted on

Day 3: Attaching Custom Exceptions to Functions and Classes in Python

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.

# 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
Enter fullscreen mode Exit fullscreen mode

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

>>> 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
Enter fullscreen mode Exit fullscreen mode

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

>>> 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?
Enter fullscreen mode Exit fullscreen mode

Attaching the Custom Exception to Classes

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

# 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")
Enter fullscreen mode Exit fullscreen mode

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

>>> 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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (3)

Collapse
stevepryde profile image
Steve Pryde

This is really cool and could be used to clearly denote which exceptions are allowed/expected to be thrown from a particular function or method.

I like it from an ergonomics point of view (import the function and get its exception for free).

Thanks for the tip! :)

Collapse
danielfeldroy profile image
Daniel Feldroy Author • Edited

Just found out I missed a "self" in the class. Added that!

Collapse
hanpari profile image
Pavel Morava

Or just import a package or module as a name

import library as lib
lib.function ...
lib.Class...
lib.Exception...

And you don't pollute the namespace and don't need to attach an object to another object.

Still, at least I've learned why Django uses exceptions like this. Mypy screams all the time this one doesn't exist.