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

Note

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.

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

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

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__, __type_params__, merge __dict__, and set __wrapped__. The attributes copied and the behavior are identical.
  2. 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__ using functools.partial(self.__call__, obj) to create the bound method.
  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 bound version for method usage).
  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 pure Python implementations of staticmethod and classmethod in the Python documentation both call functools.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.