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 attribute lists (WRAPPER_ASSIGNMENTS and WRAPPER_UPDATES), and produces the same __wrapped__ reference. Per the official Python documentation, update_wrapper may be used with any callable, not just functions. 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)
# Hello, Ada
functools.update_wrapper(self, func) does the same work as @functools.wraps(func) on a function wrapper. The Python 3 functools documentation lists the attributes copied directly via WRAPPER_ASSIGNMENTS: __module__, __name__, __qualname__, __doc__, __annotations__, and __type_params__ (added in Python 3.12 as part of PEP 695). WRAPPER_UPDATES specifies what is merged: __dict__ (the instance dictionary). __wrapped__ is then set to point to the original callable. Notably, update_wrapper also returns the wrapper object, which means it can be used as part of an assignment expression. The result is identical to what @functools.wraps produces.
The set of attributes copied by update_wrapper has grown across Python versions. Python 3.2 added automatic __wrapped__ assignment, added __annotations__ to the default copy list, and stopped raising AttributeError for missing attributes on the wrapped callable. Python 3.4 (bpo-17482) strengthened __wrapped__ so it always refers to the original function, even if that function itself defines a __wrapped__ attribute. Python 3.12 added __type_params__ to support PEP 695 generic functions. If you are targeting Python 3.11 or earlier, omit __type_params__ from any custom WRAPPER_ASSIGNMENTS tuple. Sources: docs.python.org/3/library/functools.html
def statement. The wrapper function is being created right now. wraps decorates it at definition time before it is assigned to a name.attributes
copied
__init__. The wrapper object (self) already exists. There is no def to place @ above, so the imperative call is the only option.@CountCalls decorator and the subsequent metadata lookups. Each step shows the live state of the object.The Python Descriptor Guide (available at docs.python.org/3/howto/descriptor.html) shows pure Python equivalents of both staticmethod and classmethod that call functools.update_wrapper(self, f) in their __init__ methods. These implementations emulate PyStaticMethod_Type() and PyClassMethod_Type() from Objects/funcobject.c in CPython. This is not a workaround—it is the standard, officially documented approach to metadata preservation on class-based wrappers.
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 non-data descriptors—they implement __get__, which is what allows Python to bind them as methods when accessed as class attributes. The Python Descriptor Guide documents that functions implement __get__() specifically so they can be converted into bound methods during attribute lookup. In CPython, a function's __get__ returns types.MethodType(self, obj)—a bound method object that pre-fills the instance as the first argument.
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
import time
class Timer:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
def __call__(self, *args, **kwargs):
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: Calculator.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
import time
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):
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 functools.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). This mirrors how Python's own attribute lookup works: when you access instance.method, object.__getattribute__ checks whether the stored object defines __get__ and, if so, invokes it with the instance and class. When obj is None (the method is accessed on the class itself, not on an instance), returning self unchanged replicates what Python's functions do. When obj is an instance, functools.partial(self.__call__, obj) pre-fills the instance as the first argument. Note that Python's built-in functions achieve the same result differently—they return a true types.MethodType object rather than a partial—but the observable behavior is identical for the decorator use case. Using partial is the documented, practical pattern for class-based descriptors where MethodType is not needed.
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).
The three-method template covers most decorator use cases. For scenarios requiring truly transparent wrapping—where the decorator must be invisible to inspect.signature, pass all introspection checks, and work uniformly across instance methods, class methods, static methods, and plain functions without separate __get__ implementations—the third-party wrapt library provides an ObjectProxy and a @wrapt.decorator factory that handle all of these cases. Its author Graham Dumpleton documents the limitations of functools.wraps-based approaches in detail at the wrapt GitHub repository. For production decorators where full signature transparency and edge-case correctness matter, wrapt is worth examining.
functools.update_wrapper(self, func) set on the class instance that tools like inspect.unwrap() rely on?- Call
functools.update_wrapper(self, func)in__init__— this copies__name__,__qualname__,__doc__,__annotations__,__module__, and__type_params__(Python 3.12+) from the wrapped function onto the class instance, merges__dict__, and sets__wrapped__. - Define
__call__with the wrapper logic — this is where your before/after behavior goes; callself.func(*args, **kwargs)to invoke the original function. - Define
__get__for method binding — returnselfwhenobj is None(class-level access), and returnfunctools.partial(self.__call__, obj)whenobjis an instance. This makes the decorator work correctly on class methods, not just standalone functions.
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
import time
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):
last_exc: Exception | None = None # Python 3.10+
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)
assert last_exc is not None
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.
When to Choose a Class Decorator Over a Function Decorator
The three-method class decorator template is more code than a function-based closure. That trade-off is worth it in specific situations, and not worth it in others. The practical decision points are:
Use a class decorator when the wrapper needs to carry state between calls. A function closure can hold state in a mutable container (a list, a dict), but it is awkward. A class decorator stores state naturally as instance attributes (self.count, self.last_called, self.cache), and that state is directly inspectable from outside: greet.count reads cleanly, while greet.__closure__[0].cell_contents does not.
Use a class decorator when the wrapper logic is complex enough to warrant internal helper methods. A function closure with 60 lines of logic and three nested helpers is harder to read and test than a class with __call__, _validate, and _format_error as separate methods. Class structure imposes natural organization.
Use a class decorator when you need subclassability. A class-based decorator can be extended: class VerboseTimer(Timer) overrides __call__ and gets __get__ and update_wrapper for free. Function-based decorators cannot be subclassed.
Use a function decorator when the wrapper is simple and stateless. A two-line wrapper that logs a call and delegates to the original function is clearest as a closure. Adding a class for this case is overhead without benefit.
Because functools.update_wrapper merges func.__dict__ into self.__dict__, any custom attributes you intend to add to the instance (self.count = 0, self.cache = {}) should be set after the update_wrapper call. If the wrapped function happens to have an attribute with the same name in its own __dict__, the merge will overwrite whatever you set beforehand. Setting instance state after the call avoids that collision.
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.
Customizing Which Attributes Are Copied
By default, update_wrapper copies the six attributes defined in functools.WRAPPER_ASSIGNMENTS and merges __dict__ per WRAPPER_UPDATES. Both are plain tuples, and both can be overridden by passing the assigned and updated keyword arguments:
import functools
# Default WRAPPER_ASSIGNMENTS (CPython 3.12+):
# ('__module__', '__name__', '__qualname__', '__doc__',
# '__annotations__', '__type_params__')
# Add __defaults__ and __kwdefaults__ to the copy list
EXTENDED_ASSIGNMENTS = functools.WRAPPER_ASSIGNMENTS + (
'__defaults__', '__kwdefaults__'
)
class Logged:
def __init__(self, func):
functools.update_wrapper(self, func, assigned=EXTENDED_ASSIGNMENTS)
self.func = 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 functools.partial(self.__call__, obj)
@Logged
def connect(host="localhost", port=5432):
"""Connect to a host."""
pass
print(connect.__defaults__) # ('localhost', 5432)
print(connect.__kwdefaults__) # None
The more common customization is in the other direction: stripping attributes you do not want copied. If the original function has no annotations and you do not want update_wrapper to copy an empty __annotations__ dict onto your wrapper, pass a narrowed assigned tuple. Any attribute listed in assigned that is missing from the wrapped callable is silently skipped (as of Python 3.2), so this is safe.
Missing attributes in assigned are silently ignored, but missing attributes in updated are not — they raise AttributeError. This distinction exists because updated involves calling .update() on an existing attribute (like __dict__), which requires that attribute to already exist on the wrapper. assigned is a simple setattr, which always works. The default WRAPPER_UPDATES = ('__dict__',) is safe for all Python objects, but if you pass a custom updated tuple containing something other than __dict__, make sure the wrapper already has that attribute.
What inspect.signature Sees and How Stacking Affects It
One of the practical reasons __wrapped__ matters is that inspect.signature() follows it automatically. When you call inspect.signature(greet) on a decorated function, Python does not report (*args, **kwargs) — it follows the __wrapped__ chain to the original and reports its real signature. This is what makes decorated functions transparent to documentation generators, type checkers, and test frameworks:
import functools, inspect
class CountCalls:
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.__call__, obj)
@CountCalls
def greet(name: str, greeting: str = "Hello") -> str:
"""Return a greeting string."""
return f"{greeting}, {name}"
print(inspect.signature(greet))
# (name: str, greeting: str = 'Hello') -> str
print(greet.__wrapped__)
# <function greet at 0x...>
print(inspect.unwrap(greet))
# <function greet at 0x...>
When class decorators are stacked, each layer must call update_wrapper so that __wrapped__ forms a chain. inspect.unwrap() follows the entire chain to the innermost original. If any intermediate layer omits update_wrapper, that layer breaks the chain and inspect.signature() will report the outer wrapper's (*args, **kwargs) signature instead of the original:
import functools
import inspect
import time
class Timer:
def __init__(self, func):
functools.update_wrapper(self, func) # sets __wrapped__ = func
self.func = func
def __call__(self, *args, **kwargs):
t = time.perf_counter()
result = self.func(*args, **kwargs)
print(f"took {time.perf_counter()-t:.4f}s")
return result
def __get__(self, obj, objtype=None):
if obj is None:
return self
return functools.partial(self.__call__, obj)
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func) # sets __wrapped__ = Timer instance
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.__call__, obj)
@CountCalls
@Timer
def process(data: list) -> int:
"""Process a list and return its length."""
return len(data)
# inspect follows: CountCalls.__wrapped__ -> Timer.__wrapped__ -> process
print(inspect.signature(process))
# (data: list) -> int
print(inspect.unwrap(process))
# <function process at 0x...>
# Decorator state is on the outer wrapper
print(process.count) # 0 (before any calls)
process([1, 2, 3])
print(process.count) # 1
The stacking order matters for both behavior and state access. In @CountCalls @Timer, the outer decorator (CountCalls) is what the name process refers to, so process.count is accessible. Timer's instance is reachable via process.__wrapped__. The execution order when process() is called is CountCalls.__call__ first, then Timer.__call__, then the original function — outermost first, same as function-based stacking.
Advanced Patterns and Edge Cases
The three-method template covers the common case well. Several real-world scenarios push past what it handles correctly: code that inspects method-ness explicitly, multithreaded environments where decorator state is mutated per call, decorators that need to know their attribute name, and codebases that require full signature transparency through all decorator layers. Each of these has a specific solution that the basic pattern does not provide.
types.MethodType vs functools.partial in __get__
The standard recommendation for __get__ uses functools.partial(self.__call__, obj). This works correctly for calling the method. It breaks one thing: inspect.ismethod() returns False for a partial object. Python's own functions return a types.MethodType instance from __get__, and inspect.ismethod() returns True for those. If any code in your stack calls inspect.ismethod() — some test frameworks, serializers, and introspection utilities do — the partial approach silently breaks it:
import functools, inspect, types
# partial-based __get__ (standard approach)
class LoggedPartial:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = 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 functools.partial(self.__call__, obj)
# MethodType-based __get__ (semantically correct)
class LoggedMethod:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = 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 types.MethodType(self, obj)
class MyClass:
@LoggedPartial
def method_a(self):
pass
@LoggedMethod
def method_b(self):
pass
obj = MyClass()
print(inspect.ismethod(obj.method_a)) # False -- partial
print(inspect.ismethod(obj.method_b)) # True -- MethodType
# Both call correctly:
obj.method_a() # calling method_a
obj.method_b() # calling method_b
The trade-off: types.MethodType(self, obj) makes the decorator instance itself callable as a method, which means __call__ will receive obj as its first positional argument (the bound instance). This is exactly what you want when the decorated function expects self. Use types.MethodType when any part of your stack calls inspect.ismethod(). Use functools.partial when you need simpler reasoning about argument flow and do not depend on method-ness checks.
Thread Safety for Stateful Class Decorators
The class decorator's main advantage over a function closure is natural state storage. That advantage comes with a hidden cost: shared mutable state on a class instance is not thread-safe. The CountCalls pattern uses self.count += 1, which expands to a read, an increment, and a write — three separate operations. In a multithreaded server, two threads can both read count before either writes back, losing one increment. The CPython GIL does not protect compound operations.
There are two practical solutions depending on whether you want shared state across all threads or isolated state per thread:
import functools, threading
# OPTION A: shared counter, protected by a lock
class CountCallsThreadSafe:
"""Track total calls across all threads."""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
self._lock = threading.Lock()
def __call__(self, *args, **kwargs):
with self._lock:
self.count += 1
return self.func(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
return self
return functools.partial(self.__call__, obj)
# OPTION B: per-thread counter using threading.local
class CountCallsPerThread:
"""Track calls separately per thread."""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self._local = threading.local()
@property
def count(self):
return getattr(self._local, 'count', 0)
def __call__(self, *args, **kwargs):
self._local.count = self.count + 1
return self.func(*args, **kwargs)
def __get__(self, obj, objtype=None):
if obj is None:
return self
return functools.partial(self.__call__, obj)
@CountCallsThreadSafe
def fetch(url):
"""Fetch a URL."""
pass
# count is safe to read from any thread after concurrent calls
Option A is appropriate when you need a single accurate total across all callers — a rate limiter, a circuit breaker, a shared cache hit counter. Option B is appropriate when each thread needs its own view of state — per-request logging, thread-local caches, diagnostic counters in a worker pool. Note that the _lock attribute set in Option A is added after update_wrapper, which is correct: the __dict__ merge during update_wrapper would overwrite anything set before it if the wrapped function happened to have a _lock attribute of its own.
In Option A, the lock wraps only the counter increment, not the wrapped function call. Holding the lock during self.func(*args, **kwargs) would serialize all calls through the decorator, defeating the purpose of threading entirely. Increment under the lock, then release before calling the original function.
__set_name__ for Attribute-Aware Class Decorators
When a descriptor is assigned to a class attribute, Python 3.6+ automatically calls __set_name__(self, owner, name) on it, passing the class it was assigned to and the name of the attribute. Class-based decorators used as class-level descriptors can implement __set_name__ to gain access to this information without any manual wiring. This is particularly useful for validation decorators, lazy computed properties, and any decorator that needs to generate meaningful error messages referencing the attribute's actual name:
import functools
class Validated:
"""Decorator that enforces a predicate and names the failing field."""
def __init__(self, func=None, *, predicate=None):
self._predicate = predicate
self._attr_name = None # filled by __set_name__
if func is not None:
functools.update_wrapper(self, func)
self.func = func
else:
self.func = None
def __call__(self, func_or_instance, *args, **kwargs):
# Called either as @Validated(predicate=...) factory step
# or as the actual method wrapper
if self.func is None:
# first call: receiving the function
functools.update_wrapper(self, func_or_instance)
self.func = func_or_instance
return self
# second call: the actual method invocation
result = self.func(func_or_instance, *args, **kwargs)
if self._predicate and not self._predicate(result):
name = self._attr_name or self.func.__name__
raise ValueError(
f"{name} returned a value that failed validation: {result!r}"
)
return result
def __set_name__(self, owner, name):
# Python 3.6+ calls this automatically when the class body finishes
self._attr_name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return functools.partial(self.__call__, obj)
class Report:
@Validated(predicate=lambda v: isinstance(v, str) and len(v) > 0)
def title(self):
"""Return the report title."""
return "" # empty string -- will fail validation
r = Report()
try:
r.title()
except ValueError as e:
print(e)
# title returned a value that failed validation: ''
# -- the error names the attribute 'title' because __set_name__ ran
__set_name__ is invoked by type.__new__ during class creation, after the class body has been fully evaluated. It runs once per class definition, not once per instance. If you dynamically add a descriptor to a class after the class is created (MyClass.method = SomeDecorator(func)), __set_name__ is not called automatically — you would need to call it explicitly: SomeDecorator.__set_name__(decorator_instance, MyClass, 'method'). This is documented behavior in the Python data model reference.
What wrapt Solves That the Standard Pattern Cannot
The wrapt library by Graham Dumpleton addresses specific failure modes in the standard three-method template. Understanding what those failures are tells you when wrapt is actually necessary versus when it is overhead.
The standard template fails inspect.isfunction() and bound-method inspect.isroutine() checks. A class instance is not a function type. Even with update_wrapper copying all metadata, inspect.isfunction(decorated) returns False. The decorator itself satisfies inspect.isroutine() at the class level (because implementing __get__ makes it a method descriptor), but a bound method returned via functools.partial from __get__ does not — inspect.isroutine(instance.method) returns False. wrapt.ObjectProxy proxies all attribute access to the wrapped callable, so the wrapped object passes isinstance checks against the original type and responds correctly to function-oriented introspection at all access levels. This matters when decorating functions that are later passed to APIs that use inspect.isfunction() as a gate — some plugin systems, dependency injection frameworks, and serialization libraries do this.
The standard template requires special-casing for @classmethod and @staticmethod targets. If you apply a class decorator to a classmethod or staticmethod, the descriptor protocol interacts in a way that breaks the functools.partial binding. The stacking order matters and is not obvious. @wrapt.decorator handles these cases uniformly without requiring the decorator author to write separate logic for each.
The standard template does not propagate __class_getitem__. Generic classes (using class Foo[T] syntax from PEP 695 or the older class Foo(Generic[T])) define __class_getitem__ for subscript syntax like Foo[int]. functools.update_wrapper does not copy __class_getitem__ — it is not in WRAPPER_ASSIGNMENTS. A class decorator applied to a generic class silently breaks subscript access unless you copy it manually. wrapt.ObjectProxy forwards all attribute access transparently, including __class_getitem__, without needing explicit enumeration.
# pip install wrapt
import functools, inspect, wrapt
# Standard three-method template
class StandardLogged:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = 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 functools.partial(self.__call__, obj)
# wrapt-based decorator
@wrapt.decorator
def wrapt_logged(wrapped, instance, args, kwargs):
print(f"calling {wrapped.__name__}")
return wrapped(*args, **kwargs)
def plain_func(x):
"""Return x squared."""
return x * x
standard_wrapped = StandardLogged(plain_func)
wrapt_wrapped = wrapt_logged(plain_func)
print(inspect.isfunction(standard_wrapped)) # False
print(inspect.isfunction(wrapt_wrapped)) # True
print(inspect.signature(standard_wrapped)) # (x)
print(inspect.signature(wrapt_wrapped)) # (x)
# Both call correctly:
standard_wrapped(3) # calling plain_func
wrapt_wrapped(3) # calling plain_func
The concrete recommendation: use the three-method template for application-level decorators where you control all the code that interacts with the decorated callable. Use wrapt when building library decorators that will be applied to code you do not control, or when the decorated functions are passed to third-party APIs that perform function-type checks.
The Logged decorator below is meant to log calls and work correctly as both a function decorator and a method decorator. It has exactly one bug. Study each line carefully and identify what is wrong.
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__, and__type_params__(Python 3.12+), merge__dict__, and set__wrapped__. The function returns the wrapper object. The attributes copied and the behavior are identical tofunctools.wraps, which is itself defined aspartial(update_wrapper, ...).- Class-based decorators need
__get__to work on methods: Without the descriptor protocol,object.__getattribute__has no__get__to invoke on the decorator instance, so the binding step is skipped and the class instance is not passed as the first argument. Implement__get__usingfunctools.partial(self.__call__, obj)to create the bound behavior. - The three-method template for class decorators is:
__init__(stores the function, callsupdate_wrapper),__call__(executes the wrapper logic), and__get__(returns a partial-bound version for method usage). Python's own functions usetypes.MethodTypefor binding;functools.partialachieves the same observable result for class-based decorators. - 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 Python Descriptor Guide at docs.python.org/3/howto/descriptor.html shows pure Python implementations of
staticmethod(emulatingPyStaticMethod_Type()) andclassmethod(emulatingPyClassMethod_Type()) that both callfunctools.update_wrapper(self, f)in__init__. __wrapped__is followed by the entire introspection chain:inspect.unwrap(),inspect.signature(), andfunctools.singledispatchall follow the__wrapped__chain. It was added in Python 3.2 and hardened in Python 3.4 (bpo-17482) to always point to the original function regardless of whether that function defines its own__wrapped__.WRAPPER_ASSIGNMENTSandWRAPPER_UPDATESare overridable: Pass a customassignedtuple to copy additional attributes like__defaults__or__kwdefaults__. Missing attributes inassignedare silently skipped (Python 3.2+). Missing attributes inupdatedraiseAttributeErrorbecause they require calling.update()on an existing attribute. Set any instance state after callingupdate_wrapperto avoid the__dict__merge overwriting your own attributes.- Stacked class decorators each need their own
update_wrappercall: When decorators are stacked, each layer sets__wrapped__to point to the previous layer.inspect.signature()follows the entire chain to report the original function's signature. Any layer that omitsupdate_wrapperbreaks the chain, and introspection tools will report(*args, **kwargs)instead of the real signature. The outermost decorator is what the function name resolves to, so its state attributes are directly accessible; inner decorator state is reachable via__wrapped__. - Choose
types.MethodTypeoverfunctools.partialin__get__when method-ness matters:functools.partialobjects returnFalsefrominspect.ismethod().types.MethodType(self, obj)returnsTrue. Usepartialfor simplicity when nothing in your stack checksinspect.ismethod(); useMethodTypewhen it does — some test frameworks, serializers, and plugin systems use this check as a gate. - Stateful class decorators require explicit thread-safety:
self.count += 1is not atomic. Usethreading.Lockaround the mutation for a shared-across-threads counter, orthreading.localfor per-thread isolation. Always set the lock or local storage attributes after callingupdate_wrapperto avoid the__dict__merge overwriting them. - Implement
__set_name__when the decorator needs to know its attribute name: Python 3.6+ calls__set_name__(self, owner, name)automatically on any descriptor assigned in a class body, passing the owning class and the attribute name. This is useful for validation decorators, lazy-property patterns, and any decorator that generates error messages referencing the method's name. If the descriptor is added dynamically after class creation,__set_name__must be called manually. wraptsolves specific failures the standard template cannot: The three-method template produces an object that failsinspect.isfunction()and, for bound method access viafunctools.partial, also failsinspect.ismethod()andinspect.isroutine(); it does not forward__class_getitem__for generic class targets; and it requires manual special-casing for@classmethodand@staticmethodtargets.wrapt.ObjectProxyand@wrapt.decoratorhandle all of these transparently. Use the standard template for application-level decorators; usewraptfor library decorators applied to unknown third-party code.- Choose a class decorator over a function decorator when you need state, organization, or subclassability: Instance attributes store call counts, caches, and configuration naturally. Complex wrapper logic benefits from being organized into methods. Class-based decorators can be subclassed; function closures cannot. For simple, stateless wrapping, a function closure is cleaner.
The answer to "what is the functools.wraps equivalent for classes" is functools.update_wrapper—the same function that wraps calls internally. Per the Python 3 functools documentation, wraps is defined as partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated), making the relationship explicit: wraps is convenience syntax for update_wrapper, not a separate mechanism. 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.