Making a class decorator PREMIUM

Series: Decorators
Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
2 min. read 3 min. video Python 3.10—3.14
Python Morsels
Watch as video
02:31

Let's talk about how to make class decorators.

What are class decorators again?

Here we have a class called Point:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

Instances of this Point class don't have a nice string representation (yet):

>>> p = Point(1, 2)
>>> p
<__main__.Point object at 0x7f2a7ad11250>

I would like to make a class decorator (called easy_repr) that will make a nice string representation for whatever class it decorates:

@easy_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

So this easy_repr decorator should make a __repr__ method on our Point class, that will return the name of the class and all attributes that exist on our instance of that class.

Remember, that decorator syntax above is equivalent to this:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

Point = easy_repr(Point)

So putting @easy_repr above our class definition is the same as defining a class, then passing our class object into our class decorator, and pointing our class's name (Point) to the return value of our decorator (essentially replacing our original class by the class returned by our decorator).

What are class decorators supposed to do?

Class decorators should accept a class and return a class, the same way function decorators should accept a function and return a function.

Our class decorator needs to make a __repr__ function that will act as a __repr__ method on our class.

def easy_repr(cls):
    def __repr__(self):
        return f"{type(self).__name__} object with attributes {self.__dict__}"
    # ...

This decorator will accept a class object (not an instance of a class, but the class object itself), take this new __repr__ function, stick it on the class (effectively turning it into a __repr__ method):

    cls.__repr__ = __repr__

Finally, then we can return the class:

    return cls

So our class decorator accepts a class, adds a new __repr__ method to that class, and then returns the given class.

def easy_repr(cls):
    def __repr__(self):
        return f"{type(self).__name__} object with attributes {self.__dict__}"
    cls.__repr__ = __repr__
    return cls

Class decorators don't usually make a new class

Function decorators usually define a new function that wraps around the original function. But class decorators tend to modify the incoming class and return the same class; they don't tend to make a new class dynamically.

Class decorators tend to modify the income class instead of making a new one because it's tricky to dynamically make a new class in Python and it's especially tricky to dynamically make a new class that's meant to be the copy of another class.

So to use this easy_repr class decorator we would use the @ syntax to put it above our class definition:

@easy_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

Now instances of our class have a friendly string representation thanks to our class decorator:

>>> p = Point(1, 2)
>>> p
Point object with attributes {'x': 1, 'y': 2}

Summary

To make a class decorator in Python you need to make a function that accepts a class object, modifies that class object in some useful way, and returns the same class object.

Now it's your turn! 🚀

We don't learn by reading or watching. We learn by doing. That means writing Python code.

Practice this topic by working on these related Python exercises.

Python Morsels
Watch as video
02:31
This is a free preview of a premium screencast. You have 1 preview remaining.