Function-based decorators use closures to wrap behavior around a target function. That works well until you need to track state between calls, expose methods on the decorator itself, or organize complex configuration logic. At that point, a class whose instances are callable through the __call__ method becomes the cleaner alternative. This article covers the full pattern for building class-based decorators in Python, from the basic __init__ / __call__ structure through preserving metadata, handling method decoration, and adding configuration arguments.
Why __call__ Makes a Class Instance Callable
In Python, any object with a __call__ method is callable. You can verify this with the built-in callable() function:
class Greeter:
def __init__(self, greeting):
self.greeting = greeting
def __call__(self, name):
return f"{self.greeting}, {name}!"
hello = Greeter("Hello")
print(callable(hello))
print(hello("reader"))
The instance hello behaves like a function. You call it with parentheses and arguments, and Python routes the call to __call__. Because a decorator is any callable that takes a function and returns a callable, a class with __call__ can serve as a decorator.
The Basic Class Decorator Pattern
The simplest class-based decorator receives the function in __init__ and wraps it in __call__:
class LogCalls:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print(f"Calling {self.func.__name__}")
result = self.func(*args, **kwargs)
print(f"{self.func.__name__} returned {result}")
return result
@LogCalls
def add(a, b):
return a + b
print(add(3, 5))
When Python encounters @LogCalls, it calls LogCalls(add), creating an instance. That instance replaces add in the module namespace. Every subsequent call to add(3, 5) actually calls LogCalls.__call__(self, 3, 5).
Preserving Metadata with update_wrapper
After decoration, add.__name__ returns "LogCalls" instead of "add" because the name now belongs to the class instance. Use functools.update_wrapper to copy the original function's metadata onto the instance:
import functools
class LogCalls:
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f"Calling {self.func.__name__}")
return self.func(*args, **kwargs)
@LogCalls
def multiply(a, b):
"""Multiply two numbers."""
return a * b
print(multiply.__name__)
print(multiply.__doc__)
print(multiply.__wrapped__)
For function-based decorators, you use @functools.wraps(func) on the inner wrapper. For class-based decorators, you call functools.update_wrapper(self, func) in __init__ because the class instance itself is the wrapper. They do the same thing — the syntax differs because wraps is a decorator and update_wrapper is a regular function call.
Stateful Decorators: Tracking Data Across Calls
The primary advantage of class-based decorators over closures is clean, readable state management. Instance attributes persist across calls naturally:
import functools
import time
class Timer:
def __init__(self, func):
self.func = func
self.total_time = 0.0
self.call_count = 0
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
start = time.perf_counter()
result = self.func(*args, **kwargs)
elapsed = time.perf_counter() - start
self.total_time += elapsed
self.call_count += 1
return result
def stats(self):
avg = self.total_time / self.call_count if self.call_count else 0
return {
"calls": self.call_count,
"total_seconds": round(self.total_time, 6),
"avg_seconds": round(avg, 6),
}
@Timer
def compute(n):
return sum(range(n))
compute(1_000_000)
compute(2_000_000)
compute(500_000)
print(compute.stats())
The stats() method is accessible directly on the decorated function because compute is a Timer instance. With a closure-based decorator, exposing extra methods requires attaching them to the wrapper function, which is less natural.
Parameterized Class Decorators
When the decorator needs configuration arguments, the class structure changes. The __init__ receives the configuration, and __call__ receives the function. The __call__ method returns a wrapper function rather than calling the function directly:
import functools
import time
class Retry:
def __init__(self, max_attempts=3, delay=1.0):
self.max_attempts = max_attempts
self.delay = delay
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, self.max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f" Attempt {attempt} failed: {e}")
if attempt < self.max_attempts:
time.sleep(self.delay)
raise last_exception
return wrapper
@Retry(max_attempts=3, delay=0.1)
def fetch_data(url):
"""Fetch data from a remote endpoint."""
raise ConnectionError(f"Cannot reach {url}")
print(fetch_data.__name__)
try:
fetch_data("https://api.example.com/data")
except ConnectionError:
print("All attempts exhausted")
The flow here is different from the non-parameterized version. @Retry(max_attempts=3, delay=0.1) first creates a Retry instance with the configuration. Python then calls that instance with fetch_data as the argument, which invokes __call__(self, func). The method returns the wrapper function, which replaces fetch_data in the namespace.
| Pattern | __init__ Receives | __call__ Receives | __call__ Returns |
|---|---|---|---|
| Non-parameterized | The function | Call-time args | The function's return value |
| Parameterized | Configuration args | The function | A wrapper function |
The Method Problem and the Descriptor Protocol
A non-parameterized class-based decorator has a known limitation: it does not work correctly on class methods. When a class instance replaces a function, it loses the ability to bind to the object it is called on. The self argument of the method never gets passed:
class LogCalls:
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f"Calling {self.func.__name__}")
return self.func(*args, **kwargs)
class Calculator:
@LogCalls
def add(self, a, b):
return a + b
c = Calculator()
try:
c.add(2, 3)
except TypeError as e:
print(f"Error: {e}")
The problem is that LogCalls instances are not descriptors. When Python looks up c.add, it finds a LogCalls instance but does not invoke the descriptor protocol to bind c as the first argument. You can fix this by implementing __get__:
import functools
from types import MethodType
class LogCalls:
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f"Calling {self.func.__name__}")
return self.func(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
return self
return MethodType(self, obj)
class Calculator:
@LogCalls
def add(self, a, b):
return a + b
c = Calculator()
print(c.add(2, 3))
The __get__ method makes the LogCalls instance participate in the descriptor protocol. When c.add is accessed, Python calls LogCalls.__get__(self, c, Calculator), which returns a bound method. The bound method prepends c as the first argument when called, so self is correctly passed to Calculator.add.
If your class-based decorator will ever be used on methods (not just standalone functions), you must implement __get__. Without it, the decorator silently receives the wrong number of arguments.
Returning a Wrapper Instead of Using __call__ Directly
An alternative to implementing __get__ is to have __call__ return a regular wrapper function instead of calling the stored function directly. Because functions are descriptors natively, the returned wrapper handles method binding automatically:
import functools
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"[{self.func.__name__}] call #{self.count}")
return self.func(*args, **kwargs)
# Alternative: return a wrapper from __init__ via __call__ acting as factory
class CountCallsSafe:
def __init__(self, func):
self.count = 0
@functools.wraps(func)
def wrapper(*args, **kwargs):
self.count += 1
print(f"[{func.__name__}] call #{self.count}")
return func(*args, **kwargs)
self.wrapper = wrapper
def __call__(self, *args, **kwargs):
return self.wrapper(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
return self
import types
return types.MethodType(self, obj)
Both approaches work. The first is more direct but requires __get__ for method compatibility. The second approach stores a closure-based wrapper and delegates to it, combining the class's state management with the closure's native descriptor behavior.
Exposing Decorator Methods to Callers
One of the strongest reasons to use a class-based decorator is that callers can interact with the decorator instance through its methods. A Timer decorator can expose stats() and reset(). A Cache decorator can expose cache_clear() and cache_info():
import functools
class Memoize:
def __init__(self, func):
self.func = func
self.cache = {}
functools.update_wrapper(self, func)
def __call__(self, *args):
if args in self.cache:
return self.cache[args]
result = self.func(*args)
self.cache[args] = result
return result
def cache_clear(self):
self.cache.clear()
def cache_info(self):
return {"size": len(self.cache)}
@Memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10))
print(fibonacci.cache_info())
fibonacci.cache_clear()
print(fibonacci.cache_info())
The fibonacci name points to a Memoize instance, so fibonacci.cache_clear() and fibonacci.cache_info() are natural method calls. With a closure-based decorator, you would need to attach these as attributes on the wrapper function, which feels less cohesive.
When to Choose Classes Over Closures
| Consideration | Function Closure | Class-Based |
|---|---|---|
| Simple wrapper, no state | Preferred — less code | Unnecessary overhead |
| Tracking calls, timing, caching | Works but awkward with nonlocal | Clean instance attributes |
| Exposing methods on the decorator | Possible but feels bolted on | Natural method interface |
| Decorating methods on classes | Works natively (functions are descriptors) | Requires __get__ implementation |
| Multiple related decorators | Separate functions | Inheritance or shared base class |
| Readability for complex logic | Deep nesting | Flat class structure |
If you are unsure, start with a function-based decorator. Refactor to a class when you find yourself using nonlocal to track mutable state across calls, or when you want the decorator to have its own public API (like reset() or stats()).
Key Takeaways
- __call__ makes instances callable: Any object with a
__call__method can be used as a decorator because decorators are just callables that take a function and return a callable. - Non-parameterized: __init__ gets the function, __call__ wraps it: The class instance replaces the function in the namespace. Every call to the decorated function routes through
__call__. - Parameterized: __init__ gets config, __call__ gets the function: The decorator expression
@Class(args)creates an instance, then Python calls that instance with the function.__call__returns a wrapper. - Use functools.update_wrapper for metadata: Call
functools.update_wrapper(self, func)in__init__to copy__name__,__doc__,__module__, and__wrapped__to the instance. - Implement __get__ for method compatibility: Without the descriptor protocol, class-based decorators do not correctly bind the instance argument when used on methods. Implement
__get__to return aMethodTypebound to the object. - Class decorators excel at stateful behavior: Call counters, execution timers, caches, and rate limiters are all naturally expressed as class attributes. The decorator instance can also expose methods like
stats(),reset(), andcache_clear()that callers can use directly.
Class-based decorators are not a replacement for function-based decorators — they are an alternative for cases where persistent state and a public method interface make the code clearer. The __call__ method is the bridge that lets an object behave like a function, and once you add functools.update_wrapper and __get__, the resulting decorator is production-ready.
Frequently Asked Questions
How do I make a class work as a decorator in Python?
Implement the __call__ method on the class. For a simple decorator, __init__ receives the function being decorated and stores it as an instance attribute. __call__ receives the call-time arguments, executes logic around the stored function, and returns its result. The class instance replaces the original function in the namespace.
When should I use a class-based decorator instead of a function?
Use a class-based decorator when you need to maintain state across multiple calls to the decorated function, when the decorator has complex configuration that benefits from instance attributes, or when you want the decorator itself to expose methods that callers can interact with (like resetting a counter or clearing a cache).
How do I preserve function metadata in a class-based decorator?
Use functools.update_wrapper(self, func) inside __init__, or if your __call__ returns a wrapper function, use @functools.wraps(func) on that wrapper. The update_wrapper function copies __name__, __doc__, __module__, __qualname__, and __dict__ from the original function to the class instance.
Why does a class-based decorator fail when decorating a method?
Class instances are not descriptors by default, so they do not bind to the instance when accessed as method attributes. The self argument of the method never gets passed. To fix this, implement the __get__ method on the decorator class so it returns a bound version of itself using types.MethodType.