Class-Based Decorators in Python

Learn how to use Python classes as decorators, why the __call__ method makes it possible, and when class-based decorators are cleaner than nested functions.

7 min read

Python decorators are not limited to functions. Any callable object can serve as a decorator, and since Python classes can define the special call method that makes instances callable, class-based decorators in Python are a natural extension of the decorator pattern. A class-based decorator uses its init method to receive the function being decorated and its call method to implement the wrapper behaviour that runs on every invocation. This structure trades the nested-function pattern from earlier articles for a class with two clearly named methods, which can be easier to read and maintain when the decorator needs to manage complex state across multiple calls.

The core insight is that a decorator is simply a callable that accepts a callable and returns a callable. A function satisfies this because functions are callable. An instance of a class that defines call also satisfies this. When Python encounters the at-syntax above a function definition, it calls the expression after the at-sign with the function as the argument and rebinds the name to the return value. If the expression evaluates to a class, Python instantiates the class with the function as the argument to init, and the resulting instance replaces the original function. When callers later invoke the decorated function, Python calls the instance's call method, which contains the wrapper logic. The mechanism is identical to function-based decorators; only the container for the logic differs.

The decision to use a class instead of a function usually comes down to state management. A function-based decorator stores state in closure variables, which works well for simple values like thresholds and counters that are set at definition time and read during calls. But when the decorator needs to modify state across calls, accumulate statistics, or expose additional methods for introspection, closure variables become awkward. A class-based decorator stores state as instance attributes, which are easier to read, mutate, and expose through named methods. The class body also provides a natural place to define helper methods that the wrapper delegates to, keeping the call method focused on the wrapping logic itself.

The basic class-based decorator pattern

The simplest class-based decorator accepts the function in its init method, stores it as an instance attribute, and implements the wrapper behaviour in its call method. The init method must accept the function as its only argument beyond self, because the at-syntax calls the class with the function as the sole argument. The call method accepts the runtime arguments and forwards them to the stored function. Here is a class-based version of the null decorator:

pythonpython
class NullDecorator:
    def __init__(self, func):
        self.func = func
 
    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

This class does exactly what the function-based null decorator does: it receives a function, stores it, and returns an object that calls the stored function when called. The difference is that the stored function is an instance attribute instead of a closure variable, and the wrapper behaviour lives in a named method instead of a nested function. The usage is identical to the function-based version: you write the at-sign followed by the class name above the function definition, and Python creates an instance and replaces the function name with it.

A more practical example is a class-based timing decorator that tracks how many times a function has been called and reports the average execution time. This kind of accumulating state is awkward in a closure because the closure would need to use a mutable container like a list to track the running total. In a class, the state is stored as instance attributes that are updated on every call:

pythonpython
import time
 
class Timer:
    def __init__(self, func):
        self.func = func
        self.calls = 0
        self.total_time = 0.0
 
    def __call__(self, *args, **kwargs):
        start = time.perf_counter()
        result = self.func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        self.calls += 1
        self.total_time += elapsed
        return result
 
    def average_time(self):
        return self.total_time / self.calls if self.calls else 0.0

After applying this timer to a function and calling it several times, you can inspect the call count and average time through the decorated function's attributes. Here is what that looks like in practice:

pythonpython
@Timer
def load_data(n):
    return list(range(n))
 
load_data(1_000_000)
load_data(1_000_000)
print(load_data.calls)
print(load_data.average_time())

The decorated name now refers to a Timer instance, so its instance attributes and methods are directly accessible. The average_time method is a clean way to expose accumulated statistics without cluttering the call method with reporting logic. If you had built this as a function-based decorator with closures, you would need to store the counter and total in a mutable container, and exposing the average would require attaching a nested function to the wrapper, which is less readable than a class method.

Preserving metadata with class-based decorators

The functools.wraps decorator is designed for function-based decorators, where you apply it to the wrapper function inside the decorator. With a class-based decorator, there is no wrapper function to decorate. Instead, you call the underlying update_wrapper function directly inside init, passing the instance itself as the wrapper and the original function as the wrapped. The update_wrapper function copies the original function's metadata onto the instance, so the instance reports the correct name and docstring when inspected.

Here is the timer class with metadata preservation added. The update_wrapper call in init copies the original function's name, docstring, and other attributes onto the timer instance. The instance then behaves as if it were the original function for the purposes of introspection:

pythonpython
import time
from functools import update_wrapper
 
class Timer:
    def __init__(self, func):
        update_wrapper(self, func)
        self.func = func
        self.calls = 0
        self.total_time = 0.0
 
    def __call__(self, *args, **kwargs):
        start = time.perf_counter()
        result = self.func(*args, **kwargs)
        self.calls += 1
        self.total_time += time.perf_counter() - start
        return result
 
    def average_time(self):
        return self.total_time / self.calls if self.calls else 0.0

The update_wrapper call must happen in init, after the original function has been received but before the instance replaces the function name. The original function reference is passed as the second argument, and the instance (self) receives the metadata. This is functionally equivalent to applying wraps in a function-based decorator, and it ensures that introspection tools see the original function's identity even though the decorated object is actually an instance of the Timer class.

When to prefer class-based decorators

Class-based decorators are not a replacement for function-based decorators; they are an alternative that suits specific situations. The function-based approach from creating your first Python decorator, using nested functions and closures, is more concise for simple wrappers that do not need mutable state beyond what closure variables provide. The timing decorator that only prints elapsed time, for example, is clearer as a nested function than as a class because the class adds boilerplate without adding functionality.

Class-based decorators become the better choice when any of the following conditions hold. First, when the decorator needs to maintain state that changes across calls, like a counter or an accumulating value. The class stores state in self, which is more visible and less surprising than using a mutable default argument or a single-element list in a closure. Second, when the decorator needs to expose additional methods for inspection or control. The average_time method on the timer class is a clean interface that callers can use to query accumulated statistics. A function-based decorator can expose similar methods by attaching them to the wrapper, but the pattern is less natural and harder to document.

Third, when the decorator logic is complex enough that the nested-function structure becomes difficult to read. A decorator that validates arguments against a schema, transforms the return value through multiple steps, and logs the result to several destinations can become a wall of nested functions. Splitting the logic across init, call, and private helper methods in a class makes each piece smaller and easier to test independently. Fourth, when the decorator needs to implement multiple related behaviours that share state. A decorator that both caches results and tracks cache hit rates has two responsibilities that can be split into separate methods on a class, sharing the cache dictionary and counters as instance attributes.

The article on real-world uses for Python decorators explores practical applications of both function-based and class-based decorators, showing how they appear in production codebases for authentication, caching, validation, and observability.

Rune AI

Rune AI

Key Insights

  • Any object with a call method can serve as a Python decorator.
  • A class-based decorator stores the original function in self during init and wraps in call.
  • Class-based decorators are easier to read than nested closures when managing state across calls.
  • Classes can expose additional methods on the decorated object for introspection or control.
  • Use functools.update_wrapper on self in init to preserve the original function's metadata.
RunePowered by Rune AI

Frequently Asked Questions

Can a Python class be used as a decorator?

Yes. Any Python object that defines the __call__ method is callable, and a decorator is simply a callable that accepts a function and returns a callable. A class whose __init__ accepts the function to be decorated and whose __call__ method implements the wrapper behaviour can serve as a decorator. This pattern is called a class-based decorator and is useful when the decorator needs to maintain state across calls.

When should I use a class-based decorator instead of a function-based one?

Use a class-based decorator when the decorator needs to maintain state that changes across calls (like a call counter or an accumulating value), when the decorator logic is complex enough that splitting it across __init__ and __call__ improves readability, or when you want to expose additional methods on the decorated function for inspection or control (like a cache_clear method). Use a function-based decorator for simple wrappers where the closure-based approach is sufficiently clear.

Conclusion

Class-based decorators trade the nested-function structure for a class with init and call methods, and the trade is worthwhile when the decorator needs readable state management or exposes additional methods. The underlying mechanism is the same: a callable receives a function and returns a callable. Understanding both the function-based and class-based forms gives you the flexibility to choose the clearest implementation for each decorator you write.