The python data class annotation was introduced to me a few months ago. It has become a go-to pattern for me for several reasons that I will share here. I’ve also been combining it with making classes callable. That could sound like I’m combining two things that ought not be combined; however, I think it’s a useful pattern that has led to modular and well-tested code.
First, let’s start with the data class annotation. Data classes were originally described as a part of pep-557. I encourage you to read through that link to learn more about them as well, but I will give a few highlights here to explain what they are.
By default, classes annotated with @dataclass get several methods for free. This leads to some really clean-looking classes. Some big things that you get for free with data classes are basic implementations of __init__, __eq__, __repr__, and others! This means that the Activity class below:
can be turned into a data class that will look like this:
and that’s it! That’s all you need and you get the rest! Now, it’s important to know what you are getting and how it is implemented (which is why pep-557 is worth a look if this seems as great to you as it does to me) but if you just want some basic methods for instance comparisons this is pretty handy.
Type annotations are also required on data classes and every property is required by the constructor unless you give it a default value. I use type hints as often as I can with python and it has saved me a decent amount of trouble. So forcing the typing of properties feels like a win.
If you’d prefer to define any of these methods yourself you can override the default functionality that the data class provides by simply implementing the method in the class. You can also override the defaults of the annotation:
It’s good to know that you can change the default behavior provided by the annotation. The python documentation on data classes explains each of these properties in greater detail here.
So far, I’ve primarily used data classes in the most basic way. I want my properties required, typed, and I don’t want to have to write a standard __init__ and __eq__ if I don’t have to. Next, I will explain how I have turned these basic classes into callable objects and why that has increased the modularity and readability of my code.
So now I’ll move on to classes as callables. To start I’ll explain how you turn a class into a callable and how that looks when it’s used.
Turning a class into a callable in python is very simple. You just need to implement the __call__ method in the class. I’ve been using this pattern with classes that are supposed to do something. It doesn’t make sense to me to call something that isn’t performing some sort of action. So as an example here let’s have a class that returns the capacity of an activity based on the grade level of the participants and the teachers available. Let’s assume that this class will have a function as a dependency that will make an external call and return a dictionary of ratios_by_grade that looks like:
Then we can create a determine capacity class that looks like this:If our code wants to use this class to calculate the max capacity of an activity it can do so like this:
If our code wants to use this class to calculate the max capacity of an activity it can do so like this:
So why would we create a class to do something like this instead of just having a python module that looks like this?
The reason that seems most important to me is that the class option has the dependencies injected in order to allow for easier testing. In order to unit test the second option you would need to patch the dependency. While this is definitely doable I’ve found the patch annotation to be confusing and not the easiest to use.
Putting them Together
So why combine data classes with callable classes? The main benefit is the readability and ease of implementation. In the example above, the dependency is required. By creating it as a data class we can get the constructor for free and the focus of the class implementation is it’s call method. We probably don’t benefit from having an __eq__ and __repr__ implemented for us with these classes. But forced typing on properties and simplified code still feels like a good enough reason to use what can be provided for us with the data class annotation.
– Liz Johnson, Senior Software Engineer at Artium
*This pattern was introduced to me by David Borenstein who taught me most of what I know about python and data analysis and sparked my interest in machine learning!