What’s the difference between calling a class decorator on a class instance and on a class definition?

class Deco:
    def __init__(self, name):
        self.name = name
    def __call__(self, test_class):
        def inner_func(whatisit):
            return whatisit
        test_class.method = inner_func
        return test_class

class TestClass:
    def __init__(self, name):
        self.name = name
    
@Deco('deco')
class TestClassWrapped:
    def __init__(self, name):
        self.name = name

test = TestClass('test')
test = Deco('deco')(test)

test_wrapped = TestClassWrapped('test')

print(test.method('whatisit')) >> whatisist
print(test_wrapped == test_wrapped.method()) >> True

Why do test.method and test_wrapped.method return different results ?
It seems that the first argument in test_wrapped.method is self, while it isn’t for test.method. Why does it differ from one to the other?

Answer

Walking through your code step-by-step:

  1. You create a regular TestClass named test.

  2. You manually call Deco and provide it with test, with the line test = Deco('deco')(test).

  3. This makes your code go through the __call__ function, which modifies the passed class test to set its method attribute to the nested function. It then returns it, and so test now contains a successfully modified TestClass : calling test.method('whatisit') will successfully return 'whatisit'. Importantly, you’re NOT accessing a method here : you’re accessing a FUNCTION through an ATTRIBUTE. self is passed to every method of classes in Python, but since this isn’t a method, it doesn’t come into play here. Try printing type(test.method), you’ll see <class 'function'> and not <class 'method'>. Importantly, you’ve passed an INSTANCE of a TestClass, not the class definition itself : and only this instance named test has had its method attribute set.

  4. You then create a TestClassWrapped named test_wrapped. Upon creating it, it enters the __call__ once more, passing it TestWrappedClass as the test_class parameter. Importantly, you’ve passed a DEFINITION of the TestWrappedClass, not an instance of it. Setting method here will modify it for every instance of TestWrappedClass you’ll later create, and can even be accessed without instantiating anything. Try calling TestClassWrapped.method("abc") : it will print abc without instantiating a TestClassWrapped at all. Interestingly, when set in this way, it’s not set as an attribute but as a method! Try printing type(test_wrapped.method). This is what I believe to be the source of confusion.

  5. In the case of print(test_wrapped.method()), you have to remember that every method of instantiated classes take self as their first parameter. This means that test_wrapped.method() will return self : hence why test_wrapped == test_wrapped.method(). Note that this doesn’t apply to methods called from a class definition, like I’ve shown earlier. TestClassWrapped.method("abc") HAS to take a parameter of some sort (like abc), else it will complain it’s lacking an argument.

So this is why test.method('whatisit') returns 'whatisit' and doesn’t take self as parameter, and why test_wrapped.method() does return self.