You write a decorator. You apply it to a function. Everything works correctly until you check my_function.__name__ and see 'wrapper' instead of 'my_function'. Your docstring is gone. The help() output describes some generic inner function instead of the function you wrote. This is one of the first surprises developers encounter when working with decorators, and understanding why it happens makes the fix immediately obvious.
The short answer: a decorator replaces your original function with a different function object. That replacement function has its own __name__, __doc__, and __qualname__ attributes, which belong to the inner function you defined inside the decorator, not the function you decorated. The fix is a single line: @functools.wraps(func) applied to the inner function. But the long answer matters because it explains how Python's function model works, why the problem exists in the first place, and what else breaks beyond just the name.
What Happens When a Function Gets Decorated
To understand the problem, you need to see what the @decorator syntax translates to at the Python level. Consider this decorator and the function it decorates:
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def calculate_tax(income, rate):
"""Calculate tax owed based on income and rate."""
return income * rate
The @log_calls line above calculate_tax is syntactic sugar. Python translates it into this assignment:
# This is what Python does internally:
calculate_tax = log_calls(calculate_tax)
After this line executes, the name calculate_tax no longer points to the original function. It points to the wrapper function that log_calls returned. The original calculate_tax still exists in memory (the wrapper closure references it through func), but the name calculate_tax in the current namespace now refers to wrapper.
This is why the metadata changes. Every Python function object carries its own set of attributes that describe it:
print(calculate_tax.__name__)
# wrapper
print(calculate_tax.__doc__)
# None
print(calculate_tax.__qualname__)
# log_calls.<locals>.wrapper
The __name__ is 'wrapper' because that is the name used in the def wrapper(*args, **kwargs): statement. The __doc__ is None because wrapper has no docstring. The __qualname__ shows the full qualified path: wrapper is a local function defined inside log_calls. None of these attributes know anything about calculate_tax because they describe the wrapper function, which is now the object that calculate_tax points to.
This is not a bug. This is exactly how Python's name binding works. The @ syntax calls the decorator and reassigns the name. The decorator returns a new function object. That new function object has its own metadata. The original function's metadata is not automatically carried over because Python has no way of knowing that you intended the wrapper to represent the original function.
The Fix: functools.wraps
The functools.wraps decorator solves this by explicitly copying metadata from the original function onto the wrapper. It is a decorator that you apply to the wrapper function inside your decorator definition.
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def calculate_tax(income, rate):
"""Calculate tax owed based on income and rate."""
return income * rate
print(calculate_tax.__name__)
# calculate_tax
print(calculate_tax.__doc__)
# Calculate tax owed based on income and rate.
print(calculate_tax.__qualname__)
# calculate_tax
One line changed: @functools.wraps(func) was added above the wrapper definition. That single line copies the following attributes from the original function onto the wrapper:
| Attribute | What It Contains | Without wraps | With wraps |
|---|---|---|---|
__name__ |
The function's simple name | 'wrapper' |
'calculate_tax' |
__doc__ |
The docstring | None |
'Calculate tax owed...' |
__qualname__ |
Qualified name including enclosing scopes | 'log_calls.<locals>.wrapper' |
'calculate_tax' |
__module__ |
The module where the function was defined | Decorator's module | Original function's module |
__annotations__ |
Type annotations from the function signature | {} |
Original annotations |
__dict__ |
Custom attributes set on the function | Empty | Updated with original's dict |
__wrapped__ |
Reference to the original function | Does not exist | Points to original function |
The __wrapped__ attribute is particularly valuable. It gives you direct access to the original, undecorated function. This is useful for testing, where you might want to verify the function's core behavior without the decorator's added logic, and for introspection tools that need to examine the original implementation.
# Access the original function, bypassing the decorator
original = calculate_tax.__wrapped__
result = original(50000, 0.25)
print(result)
# 12500.0 (no "Calling calculate_tax" printed)
How wraps Interacts With inspect.signature
One attribute that functools.wraps does not literally copy is the function signature. The wrapper function still has the parameter list (*args, **kwargs) at the bytecode level. However, Python's inspect.signature() function is aware of the __wrapped__ attribute. When inspect.signature() encounters a function with a __wrapped__ attribute, it follows the reference and reports the signature of the original function instead.
import inspect
print(inspect.signature(calculate_tax))
# (income, rate)
# Without @functools.wraps, this would show:
# (*args, **kwargs)
This means that help(), IDE autocompletion, and documentation generators all show the correct signature for decorated functions, as long as the decorator uses @functools.wraps. If you need to inspect the wrapper's own signature instead (for example, to confirm that it accepts arbitrary arguments), you can pass follow_wrapped=False to inspect.signature().
Placement in Parameterized Decorators
Parameterized decorators have three levels of nesting, which creates a common source of confusion about where @functools.wraps belongs. The rule is simple: it always goes on the innermost function, the one that replaces the original function.
import functools
def repeat(n): # outer: accepts parameters
def decorator(func): # middle: accepts the function
@functools.wraps(func) # applied HERE, on the innermost
def wrapper(*args, **kwargs): # inner: replaces the function
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
"""Print a greeting."""
print(f"Hello, {name}")
print(greet.__name__) # greet
print(greet.__doc__) # Print a greeting.
greet("Kandi")
# Hello, Kandi
# Hello, Kandi
# Hello, Kandi
Placing @functools.wraps(func) on the decorator function (the middle layer) instead of the wrapper function is a common mistake. That would attempt to copy metadata from func onto decorator, which is not the function that replaces the original. The function that takes the original's place in the namespace is wrapper, so that is where wraps must be applied.
What Breaks Without functools.wraps
The name change is the symptom that developers notice first, but it is not the only consequence of missing functools.wraps. The absence of proper metadata propagation causes problems across several categories of tools and patterns.
Debugging and Tracebacks
When an exception occurs inside a decorated function, the traceback reports the wrapper function's name and location. In a codebase with multiple decorators, every traceback shows the same inner function name, making it harder to identify which decorated function caused the error.
def validate(func):
def wrapper(*args, **kwargs):
if not args:
raise ValueError("At least one argument required")
return func(*args, **kwargs)
return wrapper
@validate
def process_order(order_id):
"""Process an order by its ID."""
return f"Processing {order_id}"
@validate
def cancel_order(order_id):
"""Cancel an order by its ID."""
return f"Cancelling {order_id}"
# Both of these produce tracebacks referencing "wrapper"
# instead of "process_order" or "cancel_order"
try:
process_order()
except ValueError as e:
import traceback
traceback.print_exc()
# Traceback shows: in wrapper
# raise ValueError("At least one argument required")
With @functools.wraps(func) added to wrapper, the traceback still shows wrapper in the decorator's own code, but help(), logging output, and any code that reads __name__ will correctly identify which function was involved.
Documentation Generators
Tools like Sphinx and pdoc read function metadata to auto-generate documentation pages. Without functools.wraps, every decorated function in your project appears with the name wrapper, no docstring, and a generic (*args, **kwargs) signature. The generated documentation becomes useless for any function that uses a decorator.
Logging and Monitoring
Production logging systems commonly record function names for tracing and performance monitoring. If decorated functions all report as wrapper, log analysis tools cannot distinguish between different decorated functions. This is particularly problematic in web frameworks where route handlers, middleware, and signal handlers are all decorated.
import functools
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def monitor(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logger.info("Entering %s", func.__name__)
result = func(*args, **kwargs)
logger.info("Exiting %s", func.__name__)
return result
return wrapper
@monitor
def sync_inventory(warehouse_id):
"""Synchronize inventory data for a warehouse."""
return f"Synced {warehouse_id}"
sync_inventory("WH-042")
# INFO:__main__:Entering sync_inventory
# INFO:__main__:Exiting sync_inventory
This logging decorator demonstrates a pattern where functools.wraps serves double duty. It preserves the metadata on the function object, and the decorator's own code uses func.__name__ to produce meaningful log messages. Without wraps, the func.__name__ reference inside the decorator still works correctly because it reads from the original function (captured in the closure). But the external-facing metadata on the function object would be wrong.
Testing and Mocking
Testing frameworks rely on function names and module paths to target specific functions for mocking or patching. If a decorator strips the function's identity, unittest.mock.patch and similar tools may fail to locate the function or may patch the wrong object.
import functools
def cache_result(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# caching logic here
return func(*args, **kwargs)
return wrapper
@cache_result
def fetch_user(user_id):
"""Fetch a user from the database."""
return {"id": user_id, "name": "Test User"}
# In tests, bypass the cache to test the core function:
raw_result = fetch_user.__wrapped__(42)
print(raw_result)
# {'id': 42, 'name': 'Test User'}
The __wrapped__ attribute provided by functools.wraps gives tests a clean path to the original function. Without it, there is no standard way to access the undecorated version, and tests either need to import the function before it gets decorated or use fragile workarounds involving closure inspection.
Class-Based Decorators
When implementing a decorator as a class instead of a nested function, functools.wraps does not apply directly since there is no inner function to decorate. Instead, you call functools.update_wrapper in the __init__ method. This is the function that wraps calls internally.
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"{self.func.__name__} called {self.call_count} times")
return self.func(*args, **kwargs)
@CountCalls
def say_hello(name):
"""Greet someone by name."""
return f"Hello, {name}"
print(say_hello.__name__) # say_hello
print(say_hello.__doc__) # Greet someone by name.
say_hello("Kandi") # say_hello called 1 times
say_hello("Kandi") # say_hello called 2 times
functools.update_wrapper(self, func) does the same job as @functools.wraps(func), but it is called as a regular function rather than used as a decorator. The first argument is the wrapper (in this case, the class instance) and the second argument is the wrapped function. Both wraps and update_wrapper accept the same optional assigned and updated parameters for controlling exactly which attributes get copied.
If you need to preserve additional metadata beyond the default set (for example, __defaults__ or __kwdefaults__), you can extend the assigned tuple: @functools.wraps(func, assigned=functools.WRAPPER_ASSIGNMENTS + ('__defaults__', '__kwdefaults__')).
Key Takeaways
- Decorators replace functions. The
@decoratorsyntax calls the decorator function and reassigns the original name to whatever the decorator returns. The returned object has its own metadata, which defaults to describing the wrapper function rather than the original function. This is Python's name binding working as designed, not a bug. - functools.wraps copies six attributes and adds a seventh. It copies
__module__,__name__,__qualname__,__annotations__,__type_params__, and__doc__from the original function onto the wrapper. It updates the wrapper's__dict__with the original's custom attributes. And it adds__wrapped__, a direct reference to the original function. - inspect.signature follows __wrapped__. Python's
inspectmodule recognizes the__wrapped__attribute and uses it to report the original function's signature. This meanshelp(), IDE tooltips, and documentation generators all show the correct parameter list for decorated functions whenfunctools.wrapsis used. - Place @functools.wraps on the innermost function. In standard two-level decorators, it goes on the wrapper function. In three-level parameterized decorators, it goes on the innermost wrapper, not the middle decorator function. For class-based decorators, use
functools.update_wrapper(self, func)in__init__. - Treat it as non-negotiable. Every custom decorator should use
functools.wraps. The cost is one import and one line of code. The benefit is correct behavior in debuggers, documentation tools, logging systems, testing frameworks, and any code that reads function metadata. There is no reason to omit it.
The pattern to commit to memory is this:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# your logic here
return func(*args, **kwargs)
return wrapper
That is the canonical decorator template. Every decorator you write should start from this structure. The *args, **kwargs passthrough ensures compatibility with any function signature. The @functools.wraps(func) line preserves identity. The explicit return func(*args, **kwargs) ensures the original return value passes through. Get these three elements right, and your decorated functions will behave identically to their undecorated versions in every context except the additional behavior your decorator adds.