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.

Tap each row below to compare them side by side:

functools.wraps
Decorator factory (returns a decorator)
@functools.wraps(func)
functools.update_wrapper
Regular function (called imperatively)
functools.update_wrapper(wrapper, func)
functools.wraps
At wrapper function definition time (during def)
functools.update_wrapper
After the wrapper object already exists
functools.wraps
Works on functions. Does not work on class instances with @ syntax.
functools.update_wrapper
Works on both functions and class instances (the wrapper can be any object).
Both tools (same defaults)
Assigned: __module__, __name__, __qualname__, __annotations__, __type_params__ (3.12+), __doc__. Updated: __dict__. Always sets __wrapped__.
Implementation
wraps calls partial(update_wrapper, ...). update_wrapper is the actual implementation doing the work.
Python Pop Quiz

Which statement correctly describes the relationship between functools.wraps and functools.update_wrapper?

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 (this changed in Python 3.2; prior to that, missing attributes raised AttributeError). 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 as of Python 3.12 – 3.14 (per official docs):

WRAPPER_ASSIGNMENTS = (
    '__module__', '__name__', '__qualname__',
    '__annotations__', '__type_params__', '__doc__',
)
WRAPPER_UPDATES = ('__dict__',)
# Note: CPython 3.14 main branch also handles __annotate__ as part of
# PEP 649/749 (issue #124342). This is an addition alongside __annotations__,
# not a replacement. See the version history section for full details.

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 __wrapped__ ordering fix (Python 3.4)

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. Before this fix, stacking multiple decorators could cause __wrapped__ to bypass intermediate layers and point straight to the innermost function.

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.

A useful way to confirm this relationship at the Python prompt:

import functools

# wraps(func) returns a functools.partial object
decorator = functools.wraps(lambda: None)
print(type(decorator))   # <class 'functools.partial'>
print(decorator.func)    # <function update_wrapper at 0x...>

# Confirm: wraps.func IS update_wrapper
print(decorator.func is functools.update_wrapper)  # True
Python Pop Quiz

What does functools.wraps(func) return when called?

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. A common mistake is applying wraps at the wrong nesting level:

import functools

def retry(max_attempts=3):
    def decorator(func):
        @functools.wraps(func)  # correct: 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

# WRONG pattern (missing wraps on innermost):
def retry_broken(max_attempts=3):
    @functools.wraps(lambda: None)   # wrong level — decorating decorator, not wrapper
    def decorator(func):
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        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
print(greet.count)      # 0 initially

greet("Ada")
print(greet.count)      # 1

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.

Class Decorator Pitfall: Descriptor Protocol

When update_wrapper copies __dict__ from the original function onto self, it merges the function's attribute dictionary into the instance. If the original function has attributes with names that collide with class attributes or properties, those class-level attributes can be silently overwritten. Inspect the merged result with vars(greet) if unexpected attributes appear.

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

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

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

Python Pop Quiz

You are writing a class-based decorator. Inside __init__, your instance (self) is the wrapper. Which is the correct way to copy metadata from the original function?

The __wrapped__ Attribute and inspect.signature()

One of the less-documented effects of both wraps and update_wrapper is that they always set __wrapped__ on the wrapper, regardless of the assigned configuration. This attribute is the key that connects the Python introspection system to the original function.

As of Python 3.5, inspect.signature() automatically follows __wrapped__ chains to return the signature of the original function rather than the generic (*args, **kwargs) signature of the wrapper:

import functools
import inspect

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

@my_decorator
def add(x: int, y: int = 0) -> int:
    """Add two integers."""
    return x + y

# inspect.signature follows __wrapped__ automatically (Python 3.5+)
print(inspect.signature(add))   # (x: int, y: int = 0) -> int
print(add.__wrapped__)          # 

# inspect.unwrap follows the full chain to the bottom
print(inspect.unwrap(add))      # the original unwrapped add function

The inspect.unwrap() function, added in Python 3.4, traverses the full __wrapped__ chain link by link until it reaches a function with no __wrapped__ attribute. It also raises ValueError if it detects a cycle in the chain—a safeguard added precisely because __wrapped__ is set by convention and can theoretically be set to anything, including a circular reference.

The Signature Problem functools.wraps Does Not Fully Solve

There is a subtlety that many tutorials omit. functools.wraps does not copy __defaults__ or __kwdefaults__ from the original function. This is intentional: class-based wrappers do not have those attributes, and forcing them into WRAPPER_ASSIGNMENTS would break update_wrapper when used with class instances.

The Python documentation describes functools.wraps as "a convenience function for invoking update_wrapper() as a function decorator when defining a wrapper function" and confirms it is "equivalent to partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)."

Python 3.14 Standard Library — functools module — docs.python.org/3/library/functools.html

The practical consequence: if your wrapper uses *args, **kwargs and a caller inspects its signature without following __wrapped__, they see the generic signature. The fix depends on how strict you need to be:

import functools
import inspect

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

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

# inspect.signature follows __wrapped__ — reports the original (Python 3.5+)
print(inspect.signature(greet))
# (name: str, greeting: str = 'Hello') -> str

# Direct attribute access does NOT follow __wrapped__
print(greet.__defaults__)   # None  (wrapper's defaults, not greet's)

# To access original defaults, use __wrapped__
print(greet.__wrapped__.__defaults__)  # ('Hello',)

# Or extend WRAPPER_ASSIGNMENTS to include them
EXTENDED = functools.WRAPPER_ASSIGNMENTS + ('__defaults__', '__kwdefaults__')

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

@my_decorator_full
def greet2(name: str, greeting: str = "Hello") -> str:
    """Greet someone (full copy)."""
    return f"{greeting}, {name}"

print(greet2.__defaults__)  # ('Hello',)
Framework interaction: why __wrapped__ matters beyond introspection

Frameworks that dispatch based on function signature—Flask route matching, pytest fixture injection, Pyramid view configuration—call inspect.signature() on your decorated functions. If the wrapper's *args, **kwargs signature reaches the framework instead of the original signature, dispatching can behave incorrectly. Since Python 3.5, inspect.signature() follows __wrapped__ automatically, so functools.wraps protects you from this class of bug in modern Python. In Python 3.4, you had to call inspect.unwrap() manually to get the same result.

Stopping unwrap Early with __signature__

Sometimes you want inspect.signature() to report a specific signature rather than the original function's. Set __signature__ explicitly on the wrapper; inspect.signature() stops following the __wrapped__ chain as soon as it finds a __signature__ attribute:

import functools
import inspect

def get_context():
    """Placeholder: returns whatever context the application provides."""
    return {}

def inject_context(func):
    """Inject a 'ctx' argument automatically; callers do not provide it."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        kwargs['ctx'] = get_context()  # injected at call time
        return func(*args, **kwargs)

    # Override reported signature so callers see the correct interface
    original_sig = inspect.signature(func)
    params = [p for p in original_sig.parameters.values()
              if p.name != 'ctx']
    wrapper.__signature__ = original_sig.replace(parameters=params)
    return wrapper

Python Version History: What Changed and When

The behavior of update_wrapper and wraps has evolved across Python releases. Knowing this history matters when maintaining code that runs on older interpreters or when debugging unexpected behavior in version-specific environments.

__wrapped__ was added to WRAPPER_ASSIGNMENTS automatically for the first time in Python 3.2, and __annotations__ was also added to the default copied attributes. Missing attributes on the wrapped function no longer raised AttributeError—they are silently skipped. Before 3.2, you had to set __wrapped__ manually if you needed it.

inspect.unwrap() was introduced. The __wrapped__ ordering bug was fixed: the attribute is now set after the __dict__ merge so it always points to the immediate wrapper, not to a stale reference inherited from the wrapped function's own __dict__. The Python docs confirm that __wrapped__ now reliably refers to the wrapped function even when the wrapped function itself defines that attribute.

inspect.signature() gained the ability to follow __wrapped__ automatically and report the original function's signature. Before this, wrapping a function with *args, **kwargs always produced an opaque signature in IDEs and help(), even with @functools.wraps. This is the version where functools.wraps became sufficient for most real-world introspection needs.

PEP 695 introduced a new generic type syntax (def f[T](x: T) -> T: ...), and the __type_params__ attribute was added to WRAPPER_ASSIGNMENTS as part of that implementation. Any code using update_wrapper with a built-in type as the wrapped argument may encounter a TypeError on 3.12+ because type.__type_params__ is a data descriptor, not a plain attribute that setattr can overwrite. This is a CPython issue tracked upstream (cpython #119011).

PEP 649 and PEP 749 (lazy annotation evaluation) introduce __annotate__ as the underlying annotation factory on functions. The official Python 3.14 documentation (docs.python.org, last updated March 30, 2026) still lists __annotations__ in WRAPPER_ASSIGNMENTS — it has not been removed or replaced. On the CPython main branch, the implementation also copies __annotate__ from the wrapped function (CPython issue #124342), but this is an addition alongside the existing attribute handling, not a simple substitution in the tuple. A confirmed side effect on CPython 3.14.0 is that functools.update_wrapper applied to class wrappers around annotated functions can raise a PicklingError during serialization (CPython issue #143067). Code running on 3.13 and earlier is unaffected by any of these changes.

Stacking Decorators: How the __wrapped__ Chain Works in Practice

When multiple decorators are applied to a single function, each decorator that uses functools.wraps or functools.update_wrapper adds its own __wrapped__ link in a chain. Understanding how this chain behaves—and how Python traverses it—is important for debugging decorated stacks and for writing decorators that interact correctly with frameworks.

Consider three decorators applied in sequence:

import functools
import inspect

def decorator_a(func):
    @functools.wraps(func)
    def wrapper_a(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper_a

def decorator_b(func):
    @functools.wraps(func)
    def wrapper_b(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper_b

def decorator_c(func):
    @functools.wraps(func)
    def wrapper_c(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper_c

@decorator_a
@decorator_b
@decorator_c
def target(x: int) -> int:
    """The original target function."""
    return x * 2

# Python applies decorators bottom-up:
# target = decorator_a(decorator_b(decorator_c(target)))
# The outermost wrapper is wrapper_a.

# __wrapped__ forms a linked list:
print(target.__name__)                      # target  (copied from original)
print(target.__wrapped__.__name__)          # target  (wrapper_b, same metadata)
print(target.__wrapped__.__wrapped__.__name__)  # target (wrapper_c)
# target.__wrapped__.__wrapped__.__wrapped__ is the original function

# inspect.signature follows the full chain automatically
print(inspect.signature(target))            # (x: int) -> int

# inspect.unwrap walks to the bottom
print(inspect.unwrap(target) is target.__wrapped__.__wrapped__.__wrapped__)  # True

Each __wrapped__ link points to the function one layer inward. The chain length equals the number of wraps-compliant decorators applied. inspect.unwrap() always reaches the innermost original function—the one with no __wrapped__ attribute—regardless of how many layers exist.

What happens when a decorator in the middle omits functools.wraps

If one decorator in a stack omits functools.wraps, the chain is silently broken at that point. Everything below the omission becomes invisible to inspect.signature(), inspect.unwrap(), and any framework that relies on __wrapped__:

import functools
import inspect

def uses_wraps(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

def omits_wraps(func):
    # No functools.wraps — __wrapped__ is not set
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@uses_wraps     # outermost
@omits_wraps    # middle — chain breaker
@uses_wraps     # innermost
def target(x: int) -> int:
    """Original target."""
    return x

# The outermost wrapper has __wrapped__ pointing to the middle wrapper.
# The middle wrapper has NO __wrapped__, so the chain stops there.
print(target.__name__)          # wrapper  (middle wrapper's name, not original)
print(target.__doc__)           # None     (middle wrapper has no docstring)

# inspect.signature stops following at the middle wrapper
# and sees the generic (*args, **kwargs) signature
print(inspect.signature(target))  # (*args, **kwargs)

# To diagnose, walk the chain manually:
obj = target
depth = 0
while hasattr(obj, '__wrapped__'):
    depth += 1
    obj = obj.__wrapped__
print(f"Chain depth: {depth}")  # 1  (only one link visible)
One missing functools.wraps breaks the entire introspection chain

Every decorator in a stack must use functools.wraps (or functools.update_wrapper) for the full chain to be navigable. A single decorator that omits it silently truncates the chain at that layer. This is a frequent source of confusing behavior in stacked decorator setups: the outermost decorator correctly copies metadata from the next layer inward, but that layer's metadata is already wrong because it failed to copy from the original.

Circular __wrapped__ references and inspect.unwrap()

inspect.unwrap() includes protection against circular __wrapped__ references. If a chain loops back on itself, it raises ValueError instead of looping forever. You would only encounter this in deliberately adversarial or pathological code, but the protection matters in contexts where wrappers are generated programmatically:

import inspect

def f(): pass
def g(): pass
f.__wrapped__ = g
g.__wrapped__ = f   # circular chain

try:
    inspect.unwrap(f)
except ValueError as e:
    print(e)  # wrapper loop when unwrapping ...
Python Pop Quiz

Three decorators are stacked on a function. The middle decorator omits functools.wraps. What happens to inspect.signature() and inspect.unwrap() when called on the final decorated function?

Diagnostic Toolkit: Auditing a Decorated Function at Runtime

One thing you will not find documented in tutorials is a repeatable pattern for auditing a decorated function to determine exactly which attributes were preserved, whether the __wrapped__ chain is intact, and how many layers deep it runs. The following utility encapsulates everything covered so far into a single callable you can drop into any project for debugging purposes:

import functools
import inspect

def audit_wrapper(fn, *, label=None):
    """
    Print a diagnostic report for a decorated function.
    Shows: name, qualname, doc, annotations, __wrapped__ chain depth,
    signature as seen by inspect, and whether __wrapped__ reaches a
    function with no further wrapping.
    """
    tag = label or repr(fn)
    print(f"\n=== audit_wrapper: {tag} ===")
    print(f"  __name__      : {getattr(fn, '__name__', '(missing)')}")
    print(f"  __qualname__  : {getattr(fn, '__qualname__', '(missing)')}")
    print(f"  __module__    : {getattr(fn, '__module__', '(missing)')}")
    doc = getattr(fn, '__doc__', None)
    print(f"  __doc__       : {doc!r}")
    annotations = getattr(fn, '__annotations__', None)
    print(f"  __annotations__: {annotations}")
    type_params = getattr(fn, '__type_params__', '(not set — Python < 3.12 or not generic)')
    print(f"  __type_params__: {type_params}")

    # Walk the __wrapped__ chain
    chain = []
    obj = fn
    seen = set()
    while hasattr(obj, '__wrapped__'):
        if id(obj) in seen:
            chain.append('(circular reference detected)')
            break
        seen.add(id(obj))
        obj = obj.__wrapped__
        chain.append(getattr(obj, '__name__', repr(obj)))
    print(f"  __wrapped__ chain depth : {len(chain)}")
    print(f"  __wrapped__ chain names : {chain if chain else '(no __wrapped__ set)'}")

    # Signature via inspect
    try:
        sig = inspect.signature(fn)
        print(f"  inspect.signature()     : {sig}")
    except (ValueError, TypeError) as exc:
        print(f"  inspect.signature()     : ERROR — {exc}")

    # What inspect.unwrap reaches
    try:
        bottom = inspect.unwrap(fn)
        print(f"  inspect.unwrap() target : {bottom.__name__!r} (same as fn: {bottom is fn})")
    except ValueError as exc:
        print(f"  inspect.unwrap()        : ERROR — {exc}")
    print()


# --- Example usage ---
import functools

def log_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

def add_prefix(prefix):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log_call
@add_prefix("debug")
def compute(x: int, y: int = 0) -> int:
    """Add x and y."""
    return x + y

audit_wrapper(compute, label="compute (2 decorators, both use wraps)")

Running this against any decorated function immediately surfaces whether __name__ and __doc__ survived, how deep the chain runs, and whether inspect.signature() resolves to the original signature or a generic one. The chain depth is the fastest way to confirm that every layer in a decorator stack correctly called functools.wraps or functools.update_wrapper.

Using audit_wrapper in tests

You can drive audit_wrapper assertions in a test suite by capturing stdout with io.StringIO and contextlib.redirect_stdout, or by converting it into a function that returns a dict of the collected values for assertion. Either way, the pattern gives you a repeatable, structured view of wrapper state that no single built-in call provides on its own.

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) — Python 3.12 / 3.13 / 3.14
print(functools.WRAPPER_ASSIGNMENTS)
# ('__module__', '__name__', '__qualname__',
#  '__annotations__', '__type_params__', '__doc__')
# Per official Python 3.14 docs (last updated March 30, 2026), __annotations__
# remains in WRAPPER_ASSIGNMENTS. The CPython 3.14 main branch also copies
# __annotate__ as part of PEP 649/749 (issue #124342) — this is an addition,
# not a replacement. See the version history section for full details.

# Default updated attributes (merged via dict.update) — all versions
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 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)
__wrapped__ cannot be suppressed via parameters

Even when you pass a custom assigned tuple that does not include __wrapped__, update_wrapper always sets it. This attribute is hardcoded as Step 3 of the implementation and is not part of the assigned or updated configuration. If you need to prevent __wrapped__ from being set, you must call setattr manually for each attribute instead of using update_wrapper.

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__ (Python 3.12+), and __doc__ are overwritten. __dict__ is merged. __wrapped__ is always set regardless of the configured parameters.
  6. __wrapped__ powers inspect.signature(): Since Python 3.5, inspect.signature() follows __wrapped__ chains automatically. Without it, IDEs and frameworks see only the generic (*args, **kwargs) signature of the wrapper function. You can stop the chain early by setting __signature__ explicitly.
  7. __defaults__ is not copied by default: If you need direct attribute access to default values (rather than going through inspect.signature()), extend WRAPPER_ASSIGNMENTS to include __defaults__ and __kwdefaults__. Class instances cannot have these attributes, which is why they are excluded from the defaults.
  8. Python 3.12 added __type_params__: If your code runs on 3.12+ and uses generic functions (PEP 695 syntax), update_wrapper now copies type parameter metadata by default. Using type itself as the wrapped argument can raise TypeError on 3.12+ due to the descriptor nature of type.__type_params__.
  9. Python 3.14 (CPython main branch) adds __annotate__ handling alongside __annotations__: As part of PEP 649 and PEP 749 (lazy annotation evaluation), the CPython 3.14 main branch also copies __annotate__ from wrapped functions (CPython issue #124342). The official Python 3.14 documentation (last updated March 30, 2026) still lists __annotations__ in WRAPPER_ASSIGNMENTS — it has not been removed. A side effect is that class-based wrappers around annotated functions can raise PicklingError during serialization on CPython 3.14.0 (CPython issue #143067). Code on 3.13 and earlier is unaffected.
  10. One missing functools.wraps breaks the entire stacked chain: When multiple decorators are applied, every decorator in the stack must use functools.wraps (or functools.update_wrapper) for the full __wrapped__ chain to be navigable. A single decorator that omits it silently truncates the chain; layers below the break become invisible to inspect.signature(), inspect.unwrap(), and dispatch frameworks.

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.

How to Preserve Function Metadata in Python Decorators

1

Import functools

Add import functools at the top of your module. Both functools.wraps and functools.update_wrapper are part of the Python standard library and require no installation.

2

Identify your wrapper type

Determine whether your wrapper is a function defined with def — use @functools.wraps — or a class instance that is itself the wrapper — use functools.update_wrapper. This is the only meaningful decision point between the two tools.

3

Apply @functools.wraps for function-based decorators

Place @functools.wraps(func) on the line immediately above the inner wrapper function's def statement. This copies __module__, __name__, __qualname__, __annotations__, __type_params__ (Python 3.12+), __doc__, and __dict__, and sets __wrapped__ unconditionally.

4

Call functools.update_wrapper for class-based decorators

Inside the __init__ method of your decorator class, call functools.update_wrapper(self, func) after storing func as an instance attribute. This achieves identical results to @functools.wraps and works on the class instance directly.

5

Verify the metadata was copied

After applying the decorator, confirm that decorated_function.__name__, decorated_function.__doc__, and inspect.signature(decorated_function) all return the original function's values. Use inspect.unwrap(decorated_function) to confirm the __wrapped__ chain reaches the original.

6

Extend WRAPPER_ASSIGNMENTS if you need __defaults__

If something in your code reads wrapper.__defaults__ directly — rather than through inspect.signature() — pass assigned=functools.WRAPPER_ASSIGNMENTS + ('__defaults__', '__kwdefaults__') to either tool. This is optional and only needed for that specific case.

Frequently Asked Questions

What is the difference between functools.wraps and functools.update_wrapper?

functools.wraps is a decorator factory that calls functools.update_wrapper internally. The wraps function is designed to be used with @ syntax on the inner wrapper function during definition. The update_wrapper function is a regular function call that can be used on any already-existing object, including class instances. They both copy the same metadata and produce identical results; the difference is purely in how they are applied.

When should I use functools.update_wrapper instead of functools.wraps?

Use functools.update_wrapper when the wrapper is a class instance (class-based decorators), when you need to apply metadata copying after the wrapper is already created, when patching third-party decorators that omitted functools.wraps, or when working with functools.partial objects. Use functools.wraps in the standard case where you are defining a function-based decorator with an inner wrapper function.

Is functools.wraps just syntactic sugar for functools.update_wrapper?

Yes. The CPython implementation of functools.wraps is a single line: return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated). It creates a partial application of update_wrapper with the wrapped function and configuration pre-filled, then returns that partial as a decorator. When applied with @ syntax, it calls update_wrapper(wrapper, wrapped) on the decorated inner function.

What attributes do functools.wraps and update_wrapper copy?

Both copy the same attributes by default. In Python 3.12 through 3.14, the assigned attributes (directly overwritten on the wrapper) are __module__, __name__, __qualname__, __annotations__, __type_params__, and __doc__. The official Python 3.14 documentation confirms __annotations__ remains in WRAPPER_ASSIGNMENTS. The CPython 3.14 main branch additionally handles __annotate__ as part of PEP 649/749 (CPython issue #124342). The updated attributes (merged via dict.update) are __dict__ in all versions. Both also set __wrapped__ on the wrapper unconditionally. You can customize these by passing custom assigned and updated tuples.

Can I use functools.wraps on a class-based decorator?

Not directly with @ syntax, because @functools.wraps(func) is designed to decorate a function, not a class's __init__ method. For class-based decorators, call functools.update_wrapper(self, func) inside the __init__ method instead. This achieves the same result: copying metadata from the original function onto the class instance.

Does functools.wraps preserve function signatures for inspect.signature()?

functools.wraps sets the __wrapped__ attribute, and as of Python 3.5, inspect.signature() follows __wrapped__ chains automatically to return the signature of the original function. However, wraps does not copy __defaults__ or __kwdefaults__ by default because class-based wrappers lack those attributes. If your wrapper uses *args/**kwargs and you need accurate signature reporting, you can pass assigned=WRAPPER_ASSIGNMENTS + ('__defaults__', '__kwdefaults__') or set __signature__ explicitly.

What changed in Python 3.12 regarding functools.update_wrapper?

Python 3.12 added __type_params__ to WRAPPER_ASSIGNMENTS as part of the PEP 695 generic type syntax implementation. This means update_wrapper and wraps now copy generic type parameter information from the wrapped function by default. Attempting to use update_wrapper with a type object (using type as the wrapped argument) on Python 3.12+ can raise a TypeError because type.__type_params__ is a data descriptor, not a plain attribute.

What changes did Python 3.14 make to functools.update_wrapper?

The official Python 3.14 documentation (last updated March 30, 2026) still lists __annotations__ in WRAPPER_ASSIGNMENTS — it has not been removed. As part of implementing PEP 649 and PEP 749 (lazy annotation evaluation), the CPython 3.14 main branch also copies __annotate__ from wrapped functions alongside __annotations__. The __annotate__ attribute is a callable that returns annotation data lazily on demand rather than an already-evaluated dict. A confirmed side effect on CPython 3.14.0 is that class-based wrappers around annotated functions can raise a PicklingError when serialized with pickle (CPython issue #143067). Code targeting Python 3.13 and earlier is unaffected.

What happens when a decorator in a stacked decorator chain omits functools.wraps?

A single decorator that omits functools.wraps breaks the __wrapped__ chain at that layer. Every layer above the omission correctly copies metadata from the next layer inward, but that next layer's metadata is already wrong because it never copied from the original. inspect.signature() stops at the break and reports the generic (*args, **kwargs) signature. inspect.unwrap() also stops at the break. The chain can be diagnosed manually by walking obj = obj.__wrapped__ until hasattr returns False, counting how many links are visible.