Python functools.wraps Equivalent for Classes

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.

Version History: When Each Attribute Was Added

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

Mental Model: wraps vs update_wrapper
FUNCTION DECORATOR
@functools.wraps(func)
Placed above a def statement. The wrapper function is being created right now. wraps decorates it at definition time before it is assigned to a name.
same
attributes
copied
CLASS DECORATOR
functools.update_wrapper(self, func)
Called inside __init__. The wrapper object (self) already exists. There is no def to place @ above, so the imperative call is the only option.
EXECUTION TRACE
step through CountCalls.__init__ line by line
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
 
@CountCalls
def greet(name):
"""Say hello to someone."""
return f"Hello, {name}"
 
print(greet.__name__)
print(greet.__doc__)
print(greet.__wrapped__)
Click "next" to begin
This trace walks through what Python does when it processes the @CountCalls decorator and the subsequent metadata lookups. Each step shows the live state of the object.
Verified in the Python Standard Library

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.

Pro Tip

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).

When the Standard Pattern Is Not Enough

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.

CHECK YOUR UNDERSTANDING
3 questions — click an answer to get feedback, then try all options
QUESTION 1 OF 3
What does functools.update_wrapper(self, func) set on the class instance that tools like inspect.unwrap() rely on?
How to Preserve Function Metadata in a Class-Based Decorator
  1. 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__.
  2. Define __call__ with the wrapper logic — this is where your before/after behavior goes; call self.func(*args, **kwargs) to invoke the original function.
  3. Define __get__ for method binding — return self when obj is None (class-level access), and return functools.partial(self.__call__, obj) when obj is 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.

The state-access advantage in practice

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'>
Warning

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.

The one exception to silent skipping

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.

Lock granularity matters

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.

!
SPOT THE BUG
This class decorator has one mistake that will cause it to fail silently on methods. Can you find it?

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.

1import functools 2  3class Logged: 4 def __init__(self, func): 5 functools.update_wrapper(self, func) 6 self.func = func 7  8 def __call__(self, *args, **kwargs): 9 print(f"calling {self.func.__name__}") 10 return self.func(*args, **kwargs) 11  12 def __get__(self, obj, objtype=None): 13 return functools.partial(self, obj)
# CORRECTED LINE 13
def __get__(self, obj, objtype=None): return functools.partial(self.__call__, obj)

Key Takeaways

  1. 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 to functools.wraps, which is itself defined as partial(update_wrapper, ...).
  2. 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__ using functools.partial(self.__call__, obj) to create the bound behavior.
  3. The three-method template for class decorators is: __init__ (stores the function, calls update_wrapper), __call__ (executes the wrapper logic), and __get__ (returns a partial-bound version for method usage). Python's own functions use types.MethodType for binding; functools.partial achieves the same observable result for class-based decorators.
  4. 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.
  5. Class decorators (decorating classes themselves) work with both wraps and update_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 breaks isinstance() and super().
  6. 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 (emulating PyStaticMethod_Type()) and classmethod (emulating PyClassMethod_Type()) that both call functools.update_wrapper(self, f) in __init__.
  7. __wrapped__ is followed by the entire introspection chain: inspect.unwrap(), inspect.signature(), and functools.singledispatch all 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__.
  8. WRAPPER_ASSIGNMENTS and WRAPPER_UPDATES are overridable: Pass a custom assigned tuple to copy additional attributes like __defaults__ or __kwdefaults__. Missing attributes in assigned are silently skipped (Python 3.2+). Missing attributes in updated raise AttributeError because they require calling .update() on an existing attribute. Set any instance state after calling update_wrapper to avoid the __dict__ merge overwriting your own attributes.
  9. Stacked class decorators each need their own update_wrapper call: 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 omits update_wrapper breaks 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__.
  10. Choose types.MethodType over functools.partial in __get__ when method-ness matters: functools.partial objects return False from inspect.ismethod(). types.MethodType(self, obj) returns True. Use partial for simplicity when nothing in your stack checks inspect.ismethod(); use MethodType when it does — some test frameworks, serializers, and plugin systems use this check as a gate.
  11. Stateful class decorators require explicit thread-safety: self.count += 1 is not atomic. Use threading.Lock around the mutation for a shared-across-threads counter, or threading.local for per-thread isolation. Always set the lock or local storage attributes after calling update_wrapper to avoid the __dict__ merge overwriting them.
  12. 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.
  13. wrapt solves specific failures the standard template cannot: The three-method template produces an object that fails inspect.isfunction() and, for bound method access via functools.partial, also fails inspect.ismethod() and inspect.isroutine(); it does not forward __class_getitem__ for generic class targets; and it requires manual special-casing for @classmethod and @staticmethod targets. wrapt.ObjectProxy and @wrapt.decorator handle all of these transparently. Use the standard template for application-level decorators; use wrapt for library decorators applied to unknown third-party code.
  14. 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.

Frequently Asked Questions

What is the functools.wraps equivalent for class-based decorators in Python?

The equivalent is functools.update_wrapper(self, func), called inside __init__. It copies the same attributes as functools.wraps — including __module__, __name__, __qualname__, __doc__, __annotations__, and __type_params__ (Python 3.12+) — merges __dict__, and sets __wrapped__. The Python Descriptor Guide documents this as the standard pattern for class-based wrappers.

Why can't I use @functools.wraps(func) in a class-based decorator?

The @functools.wraps(func) syntax requires a def statement to decorate. In a class-based decorator, the wrapper is the class instance (self), which already exists inside __init__ — there is no function definition to place @ above. The imperative call functools.update_wrapper(self, func) inside __init__ is the correct substitute.

Why do class-based decorators break when applied to class methods?

A class instance without __get__ does not participate in the descriptor protocol. When Python accesses a method on an instance, object.__getattribute__ calls __get__ on the stored object to produce a bound method. Without __get__, the binding step is skipped and the instance is not passed as the first argument — causing TypeError on the method call. Adding __get__ with functools.partial(self.__call__, obj) restores correct binding.

What attributes does functools.update_wrapper copy?

By default it copies six attributes from the wrapped callable: __module__, __name__, __qualname__, __doc__, __annotations__, and __type_params__ (added in Python 3.12). It also merges __dict__ and sets __wrapped__. The assigned and updated parameters accept custom tuples to expand or narrow what is copied.

What does __wrapped__ do and why does it matter?

__wrapped__ is set by functools.update_wrapper to point to the original callable. Added in Python 3.2 and strengthened in Python 3.4 (bpo-17482), it is followed automatically by inspect.unwrap(), inspect.signature(), and functools.singledispatch to traverse the decorator chain and reach the original function.

Can I customize which attributes functools.update_wrapper copies?

Yes. Pass a custom tuple to the assigned parameter to control which attributes are copied directly; any listed attribute missing from the wrapped callable is silently skipped (Python 3.2+). Pass a custom tuple to updated to control which attributes are merged via .update(); a missing attribute there raises AttributeError. Always set your own instance attributes after calling update_wrapper to avoid the __dict__ merge overwriting them.

What does inspect.signature see on a class-decorated function?

inspect.signature() follows the __wrapped__ chain automatically. When functools.update_wrapper was called, it reports the original function's real signature rather than (*args, **kwargs). If any decorator layer in a stack omits update_wrapper, it breaks the chain and inspect.signature() reports the outer wrapper's generic signature instead.

When should I use wrapt instead of the standard three-method template?

Use wrapt when building library decorators that will be applied to code you do not control, or when decorated functions are passed to third-party APIs that check inspect.isfunction(), inspect.ismethod(), or inspect.isroutine(). The standard three-method template fails these checks because a class instance is not a function type. Use the three-method template for application-level decorators where you control all code that interacts with the decorated callable.