Class-based decorators use a class with __init__ and __call__ instead of nested functions. They excel at maintaining state across calls -- counting invocations, accumulating timing data, managing caches with custom eviction. But because the class instance replaces the function in the namespace, the same metadata loss problem exists: __name__, __doc__, and the function signature disappear. The fix is functools.update_wrapper, the function that @functools.wraps calls under the hood, adapted for use inside a class __init__.
How a Class-Based Decorator Works
A class-based decorator is a class that implements the __call__ method, making its instances callable. When applied with the @ syntax, Python creates an instance of the class, passing the decorated function to __init__. Every subsequent call to the decorated function invokes __call__ on that instance.
class MyDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print(f"Before {self.func.__name__}")
result = self.func(*args, **kwargs)
print(f"After {self.func.__name__}")
return result
@MyDecorator
def greet(name):
"""Return a greeting."""
return f"Hello, {name}"
# greet is now a MyDecorator instance, not a function
greet("Alice") # Before greet / Hello, Alice / After greet
After decoration, the name greet points to a MyDecorator instance. That instance stores the original function as self.func and calls it inside __call__. The decorator works, but the function's identity is gone.
The Metadata Problem
print(greet.__name__) # MyDecorator (should be "greet")
print(greet.__doc__) # None (should be "Return a greeting.")
print(type(greet)) # <class 'MyDecorator'> (not a function)
The __name__ attribute returns the class name rather than the function name. The docstring is gone. help(greet) describes a MyDecorator object with no useful information about the original function. This is the exact same metadata loss that function-based decorators suffer, but the fix is slightly different because there is no wrapper function to apply @functools.wraps to.
Applying functools.update_wrapper in __init__
The fix is a single line in __init__:
import functools
class MyDecorator:
def __init__(self, func):
functools.update_wrapper(self, func) # <-- the fix
self.func = func
def __call__(self, *args, **kwargs):
print(f"Before {self.func.__name__}")
result = self.func(*args, **kwargs)
print(f"After {self.func.__name__}")
return result
@MyDecorator
def greet(name: str) -> str:
"""Return a greeting."""
return f"Hello, {name}"
print(greet.__name__) # greet
print(greet.__doc__) # Return a greeting.
print(greet.__wrapped__) # <function greet at 0x...>
functools.update_wrapper(self, func) copies __name__, __doc__, __module__, __qualname__, and __annotations__ from the original function onto the class instance. It also merges the function's __dict__ and sets __wrapped__ to reference the original function. This is exactly what @functools.wraps(func) does in function-based decorators -- @wraps just calls update_wrapper internally.
functools.update_wrapper(self, func) must be called before any other attribute assignments that depend on the function's metadata. Placing it as the first line in __init__ after self.func = func is the safest pattern.
A Stateful Decorator: Call Counter
The primary advantage of class-based decorators is persistent state. Instance attributes survive across calls, making patterns like call counting trivial:
import functools
class CountCalls:
"""Track how many times the decorated function is called."""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
@CountCalls
def process_order(order_id: int) -> str:
"""Process a customer order."""
return f"Order {order_id} processed"
process_order(101)
process_order(102)
process_order(103)
print(process_order.count) # 3
print(process_order.__name__) # process_order
The count attribute persists on the class instance and increments with each call. In a function-based decorator, you would need a mutable container in the closure (like a list) or the nonlocal keyword to achieve the same thing. The class approach is more readable because the state is an explicit attribute with a clear name.
A Stateful Decorator: Timing Accumulator
A more advanced example accumulates execution times across calls and provides summary statistics:
import functools
import time
class TimingStats:
"""Accumulate execution timing statistics across calls."""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.call_count = 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.call_count += 1
self.total_time += elapsed
return result
@property
def avg_time(self):
if self.call_count == 0:
return 0.0
return self.total_time / self.call_count
@TimingStats
def compute_report(data: list) -> dict:
"""Generate a summary report from raw data."""
return {"count": len(data), "total": sum(data)}
for _ in range(100):
compute_report(list(range(10000)))
print(f"Calls: {compute_report.call_count}")
print(f"Total: {compute_report.total_time:.4f}s")
print(f"Average: {compute_report.avg_time:.6f}s")
print(f"Name: {compute_report.__name__}") # compute_report
The class instance acts as both the callable replacement and the statistics container. Custom properties like avg_time provide computed summaries that would be awkward to expose from a function-based decorator's closure.
Parameterized Class-Based Decorators
When a class-based decorator needs its own configuration, the pattern changes. The class __init__ receives the parameters, and __call__ receives the function. This means __call__ must return a wrapper, and update_wrapper goes on that wrapper:
import functools
class Repeat:
"""Call the decorated function n times."""
def __init__(self, n=2):
self.n = n # __init__ receives the parameter
def __call__(self, func): # __call__ receives the function
@functools.wraps(func) # @wraps works here -- it's a function def
def wrapper(*args, **kwargs):
result = None
for _ in range(self.n):
result = func(*args, **kwargs)
return result
return wrapper
@Repeat(n=3)
def say_hello(name: str) -> None:
"""Print a greeting."""
print(f"Hello, {name}")
say_hello("Alice")
# Hello, Alice
# Hello, Alice
# Hello, Alice
print(say_hello.__name__) # say_hello
Notice that in this pattern, __call__ returns a regular wrapper function, so the standard @functools.wraps(func) syntax works on that function's def line. The update_wrapper form is only needed when the class instance itself is the callable replacement (the non-parameterized pattern in the sections above).
The rule for which metadata technique to use: if the class instance replaces the function, use functools.update_wrapper(self, func) in __init__. If the class's __call__ returns a function that replaces the original, use @functools.wraps(func) on that function.
The Method Binding Problem and __get__
A class-based decorator that works on standalone functions may fail silently when applied to a method inside a class. The problem is that regular functions in Python are descriptors -- they implement __get__, which is how Python binds self to methods. A class instance does not implement __get__ by default, so the method binding mechanism breaks.
# This will fail when used on a method
class Greeter:
@CountCalls
def say_hi(self, name):
return f"Hi, {name}"
g = Greeter()
g.say_hi("Alice") # TypeError: say_hi() missing 'self' argument
The fix is to implement the __get__ method on the decorator class, making it a descriptor that participates in Python's method binding protocol:
import functools
class CountCalls:
"""Track call count -- works on functions AND methods."""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
return self
return functools.partial(self, obj)
class Greeter:
@CountCalls
def say_hi(self, name):
"""Greet someone by name."""
return f"Hi, {name}"
g = Greeter()
print(g.say_hi("Alice")) # Hi, Alice
print(g.say_hi("Bob")) # Hi, Bob
print(Greeter.say_hi.count) # 2
The __get__ method is called when the descriptor is accessed as an attribute of an instance. When obj is not None (accessed from an instance), it returns a functools.partial that pre-binds obj as the first argument, simulating the normal method binding that Python performs for regular functions. When accessed from the class directly (obj is None), it returns the decorator instance unchanged.
If you plan to use your class-based decorator on methods inside classes, you must implement __get__. Without it, the decorator will fail with a TypeError about missing the self argument. This is the single largest pitfall with class-based decorators and catches many developers off guard.
Complete Production Template
This template includes metadata preservation, descriptor protocol support, and explicit state tracking. Copy it as a starting point for any class-based decorator:
import functools
class MyClassDecorator:
"""Template for a production-grade class-based decorator."""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
# Initialize your state attributes here
def __call__(self, *args, **kwargs):
# YOUR PRE-CALL LOGIC HERE
result = self.func(*args, **kwargs)
# YOUR POST-CALL LOGIC HERE
return result
def __get__(self, obj, objtype=None):
"""Support instance method binding."""
if obj is None:
return self
return functools.partial(self, obj)
Class-Based vs. Function-Based: When to Use Which
| Criterion | Function-Based | Class-Based |
|---|---|---|
| Metadata preservation | @functools.wraps(func) on wrapper |
functools.update_wrapper(self, func) in __init__ |
| State across calls | Requires mutable closure or nonlocal |
Natural -- use instance attributes |
| Methods and properties | Transparent -- functions are descriptors | Requires __get__ implementation |
| Readability for simple cases | Compact -- two nested functions | More verbose -- class definition overhead |
| Exposing custom attributes | Set on wrapper function after definition | Natural -- define methods and properties |
| Parameterized version | Three-layer nesting | __init__ takes params, __call__ takes func |
| Testability | Bypass via func.__wrapped__ |
Bypass via instance.__wrapped__ or instance.func |
For decorators that do not need state -- such as logging, timing a single call, or access control checks -- function-based decorators are simpler and should be the default choice. For decorators that accumulate data across calls -- counters, cumulators, caches with custom eviction, or rate limiters with token buckets -- the class-based approach is cleaner because the state lives in named instance attributes rather than mutable closures.
Key Takeaways
- Use
functools.update_wrapper(self, func)in__init__. This is the class-based equivalent of@functools.wraps(func). It copies__name__,__doc__,__module__,__qualname__,__annotations__, merges__dict__, and sets__wrapped__. - Place it as the first operation in
__init__after storingself.func. This ensures all subsequent attribute access on the instance reflects the original function's metadata. - Implement
__get__for method compatibility. Without__get__, a class-based decorator fails when used on instance methods because Python cannot bindselffrom the enclosing class. Thefunctools.partial(self, obj)pattern restores correct method binding. - Class-based decorators excel at stateful behavior. Call counters, timing accumulators, caches, rate limiters, and any decorator that needs to remember information between calls benefit from instance attributes that persist naturally.
- Parameterized class-based decorators flip the pattern.
__init__receives the parameters,__call__receives and wraps the function. In this case,__call__returns a regular wrapper function, so@functools.wraps(func)applies to that function'sdefline. - Use the production template. Every class-based decorator you write should start from the three-method template:
__init__withupdate_wrapper,__call__with the wrapper logic, and__get__withfunctools.partialbinding.
Class-based decorators add a bit more structure than their function-based counterparts, but the payoff is cleaner state management and the ability to expose custom properties and methods on the decorated function. The functools.update_wrapper call preserves identity, and the __get__ method preserves method binding. Together, they make a class-based decorator behave as transparently as a function-based one while carrying persistent state that function closures struggle to express clearly.