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
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.
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)
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
wrapsisupdate_wrapperwrapped inpartial: The CPython implementation offunctools.wrapsis literallyreturn partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated). They are two interfaces to the same operation.- 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. - 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.wrapscannot be used with@syntax on a class instance. - Use
functools.update_wrapper(wrapper, func)for after-the-fact fixes: When patching third-party decorators that omitted metadata copying, callupdate_wrapperdirectly on the already-created wrapper. - Both copy the same attributes by default:
__module__,__name__,__qualname__,__annotations__,__type_params__, and__doc__are overwritten.__dict__is merged.__wrapped__is always set. - Both accept
assignedandupdatedparameters: 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. - In practice, you will use
wrapsin the vast majority of cases: Class-based decorators, manual patching, and post-hoc metadata fixes are the only situations that requireupdate_wrapperdirectly.
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.