functools.wraps is designed for function-based decorators. It uses @ syntax to decorate the inner wrapper function at definition time. When the wrapper is a class instance instead of a function, that syntax does not apply—there is no def statement to place @ above. The standard library provides functools.update_wrapper as the equivalent for classes. It performs the same metadata copy, uses the same default attributes, and produces the same __wrapped__ reference. This article covers how to use it, what extra steps class-based decorators require beyond metadata copying, and the patterns that make class decorators work correctly as both function and method wrappers.
The Problem: Why @functools.wraps Does Not Work on Classes
In a function-based decorator, @functools.wraps(func) sits above the inner wrapper function's def statement. Python compiles the function body, creates the function object, and then passes it through the wraps decorator, which copies metadata from the original function onto the newly created wrapper. The key requirement is that the wrapper is being defined at that moment—wraps decorates it at definition time.
In a class-based decorator, the wrapper is the class instance. By the time __init__ runs, self already exists. There is no def statement creating the wrapper; the instance was created by __new__ before __init__ is called. There is nowhere to place the @ syntax.
import functools
# FUNCTION-BASED: @functools.wraps works here
def function_decorator(func):
@functools.wraps(func) # decorates wrapper at def time
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# CLASS-BASED: no def to place @ above
class ClassDecorator:
def __init__(self, func):
# self already exists here -- can't use @ syntax
self.func = func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
@ClassDecorator
def greet(name):
"""Say hello."""
return f"Hello, {name}"
# Without metadata copying, identity is lost:
print(greet.__name__) # raises AttributeError
print(greet.__doc__) # None (from object.__doc__)
The class instance has no __name__ attribute of its own (Python classes have __name__, but instances do not by default). The docstring is None because the instance inherits from object, not from the original function.
The Solution: functools.update_wrapper(self, func)
The fix is to call functools.update_wrapper(self, func) inside __init__. This copies the same attributes that functools.wraps would copy, and sets __wrapped__ to point to the original function:
import functools
class CountCalls:
"""Track how many times a 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
print(f"{self.func.__name__} called {self.count} time(s)")
return self.func(*args, **kwargs)
@CountCalls
def greet(name):
"""Say hello to someone by name."""
return f"Hello, {name}"
print(greet.__name__) # greet
print(greet.__doc__) # Say hello to someone by name.
print(greet.__wrapped__) # <function greet at 0x...>
print(greet("Ada")) # greet called 1 time(s) \n Hello, Ada
functools.update_wrapper(self, func) does the same work as @functools.wraps(func) on a function wrapper. It copies __module__, __name__, __qualname__, __annotations__, __type_params__, and __doc__ from func onto self. It merges func.__dict__ into self.__dict__. And it sets self.__wrapped__ = func. The result is identical.
Python's own documentation uses this exact pattern. The Descriptor Guide in the official Python 3.14 documentation shows pure Python implementations of staticmethod and classmethod that both call functools.update_wrapper(self, f) in their __init__ methods. This is not a workaround—it is the standard, documented approach.
The Descriptor Protocol Problem
Metadata preservation is only half the story for class-based decorators. The other half is the descriptor protocol. When you use a function-based decorator, the wrapper is a function, and functions are descriptors—they implement __get__, which is what allows Python to bind them as methods when they are accessed as class attributes.
A plain class does not implement __get__. This means a class-based decorator works fine as a standalone function decorator, but fails when applied to a method inside a class:
import functools
class Timer:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
def __call__(self, *args, **kwargs):
import time
start = time.perf_counter()
result = self.func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{self.func.__name__} took {elapsed:.4f}s")
return result
class Calculator:
@Timer
def add(self, a, b):
"""Add two numbers."""
return a + b
calc = Calculator()
# This fails:
# calc.add(3, 4) -> TypeError: add() missing 1 required
# positional argument: 'b'
The error occurs because Python does not call __get__ on the Timer instance when accessing calc.add. Without __get__, the instance is not bound to calc, so self (the Calculator instance) is not automatically passed as the first argument. The call calc.add(3, 4) passes 3 as self, 4 as a, and b is missing.
Fixing It with __get__
The fix is to implement __get__ on the decorator class, making it a descriptor. When Python accesses the decorated method on an instance, __get__ returns a bound version of the decorator's __call__ method:
import functools
from functools import partial
class Timer:
"""Decorator that measures and prints execution time."""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
def __call__(self, *args, **kwargs):
import time
start = time.perf_counter()
result = self.func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{self.func.__name__} took {elapsed:.4f}s")
return result
def __get__(self, obj, objtype=None):
if obj is None:
return self
return partial(self.__call__, obj)
class Calculator:
@Timer
def add(self, a, b):
"""Add two numbers."""
return a + b
calc = Calculator()
print(calc.add(3, 4))
# add took 0.0000s
# 7
The __get__ method receives the instance (obj) and the class (objtype). When obj is None (the method is accessed on the class itself, not on an instance), it returns the decorator unchanged. When obj is an instance, it returns partial(self.__call__, obj), which pre-fills the instance as the first argument. This replicates the binding behavior that normal functions get automatically through their own __get__ method.
The complete template for a well-behaved class-based decorator that works on both functions and methods requires three things: functools.update_wrapper(self, func) in __init__ for metadata, __call__ for the wrapper logic, and __get__ with functools.partial for method binding. This three-method pattern is the class equivalent of the function-based template that uses @functools.wraps(func).
Parameterized Class Decorators
When a class-based decorator needs configuration arguments, the pattern shifts. The __init__ method receives the configuration, and __call__ receives the function. This means update_wrapper moves from __init__ to __call__, because the function is not available until __call__ is invoked:
import functools
class Retry:
"""Parameterized decorator: retry on failure."""
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):
import time
last_exc = None
for attempt in range(1, self.max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as exc:
last_exc = exc
if attempt < self.max_attempts:
time.sleep(self.delay)
raise last_exc
return wrapper
@Retry(max_attempts=5, delay=0.5)
def connect(host):
"""Connect to a remote host."""
import random
if random.random() < 0.7:
raise ConnectionError(f"Cannot reach {host}")
return f"Connected to {host}"
print(connect.__name__) # connect
print(connect.__doc__) # Connect to a remote host.
Notice that this parameterized pattern uses @functools.wraps(func) on the inner wrapper function, not functools.update_wrapper on the class instance. That is because __call__ returns a regular function (wrapper), not the class instance. The class serves as a factory that produces a standard function-based wrapper. This is a common and clean pattern: use a class for configuration storage and a function closure for the wrapper itself.
Decorating Classes (Not Just Functions)
Everything above covers decorators that are implemented as classes and applied to functions. There is a separate use case: decorators (of any kind) that are applied to classes rather than functions. When you decorate a class, functools.update_wrapper works the same way—it copies __name__, __doc__, __qualname__, and other attributes from the original class onto the wrapper:
import functools
def singleton(cls):
"""Class decorator that ensures only one instance exists."""
instances = {}
@functools.wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class DatabaseConnection:
"""Manages the database connection pool."""
def __init__(self, host="localhost", port=5432):
self.host = host
self.port = port
# Metadata is preserved even though the class is replaced by a function
print(DatabaseConnection.__name__) # DatabaseConnection
print(DatabaseConnection.__doc__) # Manages the database connection pool.
print(DatabaseConnection.__wrapped__) # <class 'DatabaseConnection'>
When you decorate a class and replace it with a function (as in the singleton example above), isinstance() checks against the original class will fail because the name now points to the wrapper function, not the class. Similarly, super() calls inside the class need the original class, which is available through DatabaseConnection.__wrapped__. Keep this in mind when decorating classes that participate in inheritance hierarchies.
Key Takeaways
functools.update_wrapper(self, func)is the class equivalent of@functools.wraps(func): Call it in__init__to copy__name__,__doc__,__qualname__,__annotations__,__module__,__type_params__, merge__dict__, and set__wrapped__. The attributes copied and the behavior are identical.- Class-based decorators need
__get__to work on methods: Without the descriptor protocol, the decorator cannot bind to the class instance when used as a method decorator. Implement__get__usingfunctools.partial(self.__call__, obj)to create the bound method. - The three-method template for class decorators is:
__init__(stores the function, callsupdate_wrapper),__call__(executes the wrapper logic), and__get__(returns a bound version for method usage). - Parameterized class decorators shift the pattern:
__init__receives configuration,__call__receives the function and returns a standard function-based wrapper using@functools.wraps(func). The class acts as a factory, not as the wrapper itself. - Class decorators (decorating classes themselves) work with both
wrapsandupdate_wrapper: The metadata copy works the same way regardless of whether the wrapped object is a function or a class. Be aware that replacing a class with a function breaksisinstance()andsuper(). - Python's standard library uses this pattern officially: The pure Python implementations of
staticmethodandclassmethodin the Python documentation both callfunctools.update_wrapper(self, f)in__init__.
The answer to "what is the functools.wraps equivalent for classes" is functools.update_wrapper—the same function that wraps calls internally. The real complexity in class-based decorators is not the metadata copying, which is a one-line call, but the descriptor protocol, which requires implementing __get__ to make the decorator work correctly as a method wrapper. Get both right, and a class-based decorator behaves identically to a function-based one, with the added advantage of natural state management through instance attributes.