Difference Between functools.wraps and functools.update_wrapper

Python's functools module provides two tools for preserving function metadata across decorators: functools.wraps and functools.update_wrapper. They do the same thing. They copy the same attributes. They produce identical results. The difference is entirely in how they are applied. wraps is a decorator you place on the inner wrapper function with @ syntax. update_wrapper is a function you call imperatively after the wrapper already exists. Understanding when to reach for each one eliminates a recurring point of confusion in Python decorator code.

The Short Answer

functools.wraps is a convenience wrapper around functools.update_wrapper. It exists so you can apply metadata copying as a decorator with @ syntax instead of making a separate function call. Both produce identical output.

Feature functools.wraps functools.update_wrapper
Type Decorator factory (returns a decorator) Regular function (called imperatively)
Usage syntax @functools.wraps(func) functools.update_wrapper(wrapper, func)
When it runs At wrapper function definition time After the wrapper object already exists
Works on functions Yes Yes
Works on class instances Not with @ syntax Yes
Attributes copied Same defaults (WRAPPER_ASSIGNMENTS + WRAPPER_UPDATES) Same defaults (WRAPPER_ASSIGNMENTS + WRAPPER_UPDATES)
Sets __wrapped__ Yes Yes
Implementation Calls partial(update_wrapper, ...) The actual implementation that does the work

What update_wrapper Does

functools.update_wrapper is the function that does the real work. It takes two required arguments—the wrapper and the wrapped function—and copies metadata from the wrapped function onto the wrapper. Here is its signature:

functools.update_wrapper(
    wrapper,                          # the object to update
    wrapped,                          # the original function
    assigned=WRAPPER_ASSIGNMENTS,     # attributes to overwrite
    updated=WRAPPER_UPDATES,          # attributes to merge
)

The function performs three operations in order. First, it iterates over the assigned tuple and copies each attribute from the wrapped function to the wrapper using setattr. If an attribute is missing on the wrapped function, it is silently skipped. Second, it iterates over the updated tuple and merges each attribute using dict.update. Third, it sets wrapper.__wrapped__ = wrapped to create the reference back to the original function.

Here is a simplified version of the CPython implementation that shows exactly what happens:

# Simplified from CPython's Lib/functools.py

WRAPPER_ASSIGNMENTS = (
    '__module__', '__name__', '__qualname__',
    '__annotations__', '__type_params__', '__doc__',
)
WRAPPER_UPDATES = ('__dict__',)

def update_wrapper(wrapper, wrapped,
                   assigned=WRAPPER_ASSIGNMENTS,
                   updated=WRAPPER_UPDATES):
    # Step 1: Copy assigned attributes (overwrite)
    for attr in assigned:
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)

    # Step 2: Merge updated attributes (dict.update)
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))

    # Step 3: Set __wrapped__ last to avoid copying it
    # from the wrapped function's __dict__ during Step 2
    wrapper.__wrapped__ = wrapped

    return wrapper
Note

The comment in CPython's source ("set __wrapped__ last so we don't inadvertently copy it from the wrapped function when updating __dict__") addresses a subtle bug fixed in Python 3.4. If a function already has a __wrapped__ attribute in its __dict__ (because it was itself decorated), the __dict__ merge in Step 2 would copy that stale reference. Setting __wrapped__ after the merge ensures the attribute always points to the immediate wrapped function, not to a function further down the chain.

How wraps Calls update_wrapper

functools.wraps is implemented as a single line of code in CPython. It uses functools.partial to pre-fill the wrapped, assigned, and updated arguments of update_wrapper, and returns the resulting partial object as a decorator:

# Actual CPython implementation of functools.wraps

def wraps(wrapped,
          assigned=WRAPPER_ASSIGNMENTS,
          updated=WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

When you write @functools.wraps(func) above a function definition, here is what happens step by step:

import functools

def my_decorator(func):
    # Step 1: wraps(func) returns partial(update_wrapper, wrapped=func)
    # Step 2: That partial is applied as a decorator to wrapper
    # Step 3: partial calls update_wrapper(wrapper, wrapped=func)
    # Step 4: update_wrapper copies metadata and returns wrapper
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# The above is exactly equivalent to:
def my_decorator_explicit(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    functools.update_wrapper(wrapper, func)
    return wrapper

Both decorators produce identical results. The only difference is readability: @functools.wraps(func) is one line at the point of definition, while the update_wrapper call is a separate statement after the wrapper is defined. For function-based decorators, wraps is the conventional choice.

When to Use wraps (Function-Based Decorators)

functools.wraps is the right choice for the standard function-based decorator pattern, where the decorator defines an inner function and returns it as the wrapper. This covers the vast majority of decorators:

import functools
import time

def timer(func):
    """Measure and print execution time."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def process(items):
    """Process a list of items."""
    return [item * 2 for item in items]

print(process.__name__)  # process
print(process.__doc__)   # Process a list of items.

This also works correctly inside parameterized decorators (decorator factories). Apply @functools.wraps(func) to the innermost wrapper—the function that replaces the original:

import functools

def retry(max_attempts=3):
    def decorator(func):
        @functools.wraps(func)  # applied to innermost wrapper
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    if attempt == max_attempts:
                        raise
        return wrapper
    return decorator

@retry(max_attempts=5)
def connect(host):
    """Establish a connection to a remote host."""
    pass

print(connect.__name__)  # connect

When to Use update_wrapper (Class-Based Decorators)

functools.update_wrapper is the correct choice when the wrapper is not a function being defined with def. The primary case is class-based decorators, where the class instance is the wrapper:

import functools

class CountCalls:
    """Track how many times a function is called."""

    def __init__(self, func):
        functools.update_wrapper(self, func)  # copy metadata to self
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

@CountCalls
def greet(name):
    """Say hello to someone."""
    return f"Hello, {name}"

print(greet.__name__)   # greet
print(greet.__doc__)    # Say hello to someone.
print(greet.__wrapped__ is greet.func)  # True

You cannot use @functools.wraps(func) in this context because there is no inner function to decorate. The class instance itself is the wrapper, and it already exists as self inside __init__. Calling functools.update_wrapper(self, func) copies metadata from func onto the class instance directly.

Patching Third-Party Decorators

update_wrapper is also the right tool when you need to fix metadata on a wrapper that was created by someone else's code:

import functools

# A third-party decorator that forgot functools.wraps
def broken_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@broken_decorator
def calculate(a, b):
    """Add two numbers."""
    return a + b

# The name is wrong because the decorator didn't use wraps
print(calculate.__name__)  # wrapper

# Fix it after the fact with update_wrapper
# (this requires that the closure still holds the original func)
# Since we can't easily get func from the closure, we need to
# know the original function or fix the decorator itself:
def fix_decorator(bad_dec):
    """Wrap a broken decorator to preserve metadata."""
    @functools.wraps(bad_dec)
    def fixed(func):
        result = bad_dec(func)
        functools.update_wrapper(result, func)
        return result
    return fixed

# Create a fixed version and re-apply
good_decorator = fix_decorator(broken_decorator)

@good_decorator
def multiply(a, b):
    """Multiply two numbers."""
    return a * b

print(multiply.__name__)  # multiply
print(multiply.__doc__)   # Multiply two numbers.
Pro Tip

A good rule of thumb: if you are writing a def inside a def, use @functools.wraps(func). If you are writing a class with __init__ and __call__, use functools.update_wrapper(self, func). If you are fixing something after the fact, use functools.update_wrapper(wrapper, original).

Customizing Which Attributes Get Copied

Both wraps and update_wrapper accept the same assigned and updated parameters, allowing you to control exactly which attributes are transferred. The defaults are defined as module-level constants:

import functools

# Default assigned attributes (overwritten on wrapper)
print(functools.WRAPPER_ASSIGNMENTS)
# ('__module__', '__name__', '__qualname__',
#  '__annotations__', '__type_params__', '__doc__')

# Default updated attributes (merged via dict.update)
print(functools.WRAPPER_UPDATES)
# ('__dict__',)

You can extend these defaults to copy additional attributes. A common example is adding __defaults__ and __kwdefaults__, which are not copied by default because class wrappers (which are also supported) do not have these attributes:

import functools

EXTENDED = functools.WRAPPER_ASSIGNMENTS + (
    '__defaults__', '__kwdefaults__',
)

def my_decorator(func):
    @functools.wraps(func, assigned=EXTENDED)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name, greeting="Hello"):
    """Greet someone."""
    return f"{greeting}, {name}"

print(greet.__defaults__)  # ('Hello',)

You can also restrict the copied attributes. If you only want to copy the name and docstring and nothing else, pass a custom tuple:

import functools

def minimal_decorator(func):
    @functools.wraps(func, assigned=('__name__', '__doc__'), updated=())
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@minimal_decorator
def example():
    """Example function."""
    pass

print(example.__name__)   # example
print(example.__doc__)    # Example function.
# __module__, __qualname__, __annotations__ are NOT copied
# __dict__ is NOT merged
# __wrapped__ IS still set (it's always added regardless)
Warning

Even when you pass a custom assigned tuple, update_wrapper always sets __wrapped__. This attribute is hardcoded in the implementation and is not part of the assigned or updated configuration. There is no way to prevent it from being set through parameters alone.

Key Takeaways

  1. wraps is update_wrapper wrapped in partial: The CPython implementation of functools.wraps is literally return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated). They are two interfaces to the same operation.
  2. Use @functools.wraps(func) for function-based decorators: This is the standard pattern. Place it directly above the inner wrapper function with @ syntax. It works for both simple decorators and parameterized decorator factories.
  3. Use functools.update_wrapper(self, func) for class-based decorators: Call it inside __init__ to copy metadata from the original function onto the class instance. @functools.wraps cannot be used with @ syntax on a class instance.
  4. Use functools.update_wrapper(wrapper, func) for after-the-fact fixes: When patching third-party decorators that omitted metadata copying, call update_wrapper directly on the already-created wrapper.
  5. Both copy the same attributes by default: __module__, __name__, __qualname__, __annotations__, __type_params__, and __doc__ are overwritten. __dict__ is merged. __wrapped__ is always set.
  6. Both accept assigned and updated parameters: You can extend the defaults to include __defaults__ and __kwdefaults__, or restrict them to copy only specific attributes. The __wrapped__ attribute is always set regardless of these parameters.
  7. In practice, you will use wraps in the vast majority of cases: Class-based decorators, manual patching, and post-hoc metadata fixes are the only situations that require update_wrapper directly.

The relationship between these two functions is straightforward once you see it: update_wrapper is the engine, and wraps is the steering wheel. One gives you the mechanics; the other gives you a comfortable interface. Pick the one that fits how you are building your decorator, and the result is identical either way.