You write a decorator, apply it to a function, and then discover that your_function.__name__ returns "wrapper" instead of the function's real name. The docstring is gone. help() shows the wrong signature. Your logging framework reports every call as coming from wrapper. This is one of the first problems every Python developer hits when working with decorators, and the fix takes exactly one line of code. This article explains why it happens, how to fix it, and the edge cases that still trip people up after the fix is in place.
The Problem: How Decoration Replaces Identity
To understand why the name changes, you need to see what the @ syntax does mechanically. When you write @my_decorator above a function definition, Python executes one specific operation: it passes the function to the decorator and rebinds the function name to whatever the decorator returns.
def my_decorator(func):
def wrapper(*args, **kwargs):
print("before")
result = func(*args, **kwargs)
print("after")
return result
return wrapper
@my_decorator
def greet(name):
"""Return a greeting for the given name."""
return f"Hello, {name}"
After Python processes the @my_decorator line, the name greet no longer points to the original function. It points to wrapper—the inner function that my_decorator returned. You can verify this directly:
print(greet.__name__) # wrapper
print(greet.__doc__) # None
print(greet.__qualname__) # my_decorator.<locals>.wrapper
The original name "greet" is gone. The original docstring "Return a greeting for the given name." is gone. The qualified name now shows the wrapper's location inside the decorator. Every tool that inspects function identity—debuggers, logging frameworks, documentation generators, test runners, serialization libraries—will see wrapper instead of greet.
This is not a bug. It is the direct, expected consequence of how Python's name binding works. The decorator returned wrapper, and Python rebound the name greet to that object. The wrapper function is a different function object with its own __name__, __doc__, and __qualname__, none of which match the original.
Where This Causes Real Problems
The lost identity is not just a cosmetic issue. It breaks concrete tools and workflows:
import logging
logger = logging.getLogger(__name__)
def log_calls(func):
def wrapper(*args, **kwargs):
# This logs "wrapper" instead of the real function name
logger.info("Calling %s", func.__name__)
return func(*args, **kwargs)
return wrapper
@log_calls
def process_payment(amount, currency):
"""Process a payment transaction."""
return {"status": "ok", "amount": amount}
# Logging output shows the correct name inside the decorator
# because we used func.__name__ (the captured reference).
# But external tools that check process_payment.__name__
# will see "wrapper":
print(process_payment.__name__) # wrapper
# help() shows wrapper's signature instead of process_payment's:
help(process_payment)
# Help on function wrapper in module __main__:
# wrapper(*args, **kwargs)
The help() output is particularly damaging. Instead of seeing the function's real parameters (amount and currency) and its docstring, users see the generic *args, **kwargs signature of the wrapper. Anyone reading your API documentation or using tab-completion in an interactive session gets useless information.
The Fix: functools.wraps
The Python standard library provides functools.wraps—a decorator that you apply to the wrapper function inside your decorator. It copies metadata from the original function onto the wrapper so that the wrapper assumes the original's identity:
import functools
def my_decorator(func):
@functools.wraps(func) # <-- one line fix
def wrapper(*args, **kwargs):
print("before")
result = func(*args, **kwargs)
print("after")
return result
return wrapper
@my_decorator
def greet(name):
"""Return a greeting for the given name."""
return f"Hello, {name}"
print(greet.__name__) # greet
print(greet.__doc__) # Return a greeting for the given name.
print(greet.__qualname__) # greet
One line of code restores the function's name, docstring, and qualified name. The help() function now reports the correct information, logging frameworks emit the real function name, and debuggers show the right call stack.
functools.wraps is itself a decorator that takes the original function as an argument. It is equivalent to calling functools.update_wrapper(wrapper, func) after the wrapper is defined. The two approaches produce identical results; wraps is the more concise form.
What functools.wraps Copies (and What It Does Not)
functools.wraps copies a specific set of attributes from the original function to the wrapper. The default set is defined by two module-level constants in functools:
| Operation | Attributes | Behavior |
|---|---|---|
| Assigned (direct copy) | __module__, __name__, __qualname__, __annotations__, __type_params__, __doc__ |
Each attribute's value is copied from the original to the wrapper, overwriting the wrapper's own value |
| Updated (dict merge) | __dict__ |
The wrapper's __dict__ is updated with entries from the original's __dict__ |
| Added | __wrapped__ |
A new attribute pointing to the original function is added to the wrapper |
What functools.wraps Does Not Copy
There are several things functools.wraps does not transfer:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def calculate(x: int, y: int = 10) -> int:
"""Add two integers."""
return x + y
# __name__ and __doc__ are correctly copied:
print(calculate.__name__) # calculate
print(calculate.__doc__) # Add two integers.
# But __defaults__ is NOT copied:
print(calculate.__defaults__) # None (original had (10,))
# The original's defaults are still accessible via __wrapped__:
print(calculate.__wrapped__.__defaults__) # (10,)
__defaults__ and __kwdefaults__ are not included in the default assigned set because a class (which can also be a wrapper) does not have these attributes, and functools.wraps is designed to work with both function and class wrappers. If you need defaults on the wrapper itself, you can extend the assigned tuple:
import functools
EXTENDED_ASSIGNMENTS = (
'__module__', '__name__', '__qualname__',
'__annotations__', '__doc__',
'__defaults__', '__kwdefaults__',
)
def my_decorator(func):
@functools.wraps(func, assigned=EXTENDED_ASSIGNMENTS)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def calculate(x: int, y: int = 10) -> int:
"""Add two integers."""
return x + y
print(calculate.__defaults__) # (10,)
Accessing the Original Function with __wrapped__
When functools.wraps copies metadata, it also adds a __wrapped__ attribute to the wrapper that points directly to the original function. This attribute has two important uses: bypassing the decorator during testing, and enabling deep introspection through the inspect module.
Bypassing the Decorator
import functools
def require_auth(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not is_authenticated():
raise PermissionError("Not authenticated")
return func(*args, **kwargs)
return wrapper
@require_auth
def get_secret_data():
"""Return classified information."""
return {"secret": "42"}
# In production, the decorator enforces authentication.
# In tests, bypass it to test the function's logic directly:
result = get_secret_data.__wrapped__()
print(result) # {"secret": "42"}
This pattern is useful in unit tests where you want to test the function's behavior in isolation, without mocking the authentication layer. The __wrapped__ attribute gives you a direct reference to the undecorated function.
inspect.signature Follows __wrapped__ Automatically
The inspect.signature() function has built-in support for __wrapped__. When it encounters a function with a __wrapped__ attribute, it follows the chain to the original function and reports that function's signature:
import functools
import inspect
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def calculate(x: int, y: int = 10) -> int:
"""Add two integers."""
return x + y
# inspect.signature follows __wrapped__ to show the original signature:
print(inspect.signature(calculate))
# (x: int, y: int = 10) -> int
# To see the wrapper's own signature, pass follow_wrapped=False:
print(inspect.signature(calculate, follow_wrapped=False))
# (*args, **kwargs)
This is why help() shows the correct signature after applying functools.wraps—it uses inspect.signature internally, which follows __wrapped__ to find the original parameter list.
Unwrapping Through Multiple Layers
When multiple decorators are stacked and each one uses functools.wraps, each layer adds its own __wrapped__ attribute forming a chain. The inspect.unwrap() function follows this chain all the way to the innermost original function:
import functools
import inspect
def decorator_a(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def decorator_b(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator_a
@decorator_b
def original(x):
"""The innermost function."""
return x * 2
# __wrapped__ on the outermost layer points to decorator_b's wrapper:
print(original.__wrapped__.__name__) # original
# inspect.unwrap follows the full chain:
innermost = inspect.unwrap(original)
print(innermost.__name__) # original
print(innermost is original.__wrapped__.__wrapped__) # True
inspect.unwrap() detects cycles in the __wrapped__ chain and raises a ValueError instead of looping forever. It also accepts an optional stop callback that lets you terminate unwrapping early—for example, stopping at the first function that has a __signature__ attribute.
Edge Cases That Still Break Things
Applying functools.wraps solves the common case, but there are several situations where metadata still gets lost or produces confusing results.
Missing functools.wraps in One Layer of a Stack
If you stack three decorators and one of them omits functools.wraps, the chain breaks at that point. The outermost decorator copies metadata from the broken layer's wrapper instead of from the original function:
import functools
def good_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def broken_decorator(func):
# Missing @functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
@broken_decorator
def original():
"""I am the original function."""
pass
# good_decorator copies metadata from broken_decorator's wrapper,
# which has the wrong name and no docstring:
print(original.__name__) # wrapper
print(original.__doc__) # None
Every decorator in a stack needs functools.wraps for the metadata to propagate correctly. A single omission anywhere in the chain poisons all the layers above it.
Class-Based Decorators Need update_wrapper
When using a class as a decorator, @functools.wraps(func) cannot be applied directly to the class because the class itself is the wrapper. Instead, use functools.update_wrapper() inside __init__:
import functools
class Retry:
"""Retry a function up to max_attempts times on failure."""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.max_attempts = 3
def __call__(self, *args, **kwargs):
for attempt in range(1, self.max_attempts + 1):
try:
return self.func(*args, **kwargs)
except Exception:
if attempt == self.max_attempts:
raise
@Retry
def flaky_operation():
"""An operation that sometimes fails."""
import random
if random.random() < 0.5:
raise ConnectionError("Temporary failure")
return "success"
print(flaky_operation.__name__) # flaky_operation
print(flaky_operation.__doc__) # An operation that sometimes fails.
functools.update_wrapper(self, func) does the same work as functools.wraps(func) but operates on an already-created object rather than decorating a function at definition time. It is the correct form when the wrapper is a class instance.
Parameterized Decorators: Which Function Gets wraps?
Parameterized decorators (decorator factories) have three layers of nesting. A common mistake is applying @functools.wraps to the wrong function:
import functools
def repeat(n=2):
"""Decorator factory: repeat the function call n times."""
def decorator(func):
# CORRECT: apply @wraps to the innermost wrapper
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(n=3)
def say_hello(name):
"""Greet someone by name."""
print(f"Hello, {name}")
print(say_hello.__name__) # say_hello
print(say_hello.__doc__) # Greet someone by name.
The rule is straightforward: @functools.wraps(func) goes on whichever function the decorator ultimately returns to replace the original. In a parameterized decorator, that is always the innermost function (the one that uses *args, **kwargs and calls func).
Third-Party Decorators You Do Not Control
Sometimes the decorator that erases your function's metadata comes from a third-party library that did not use functools.wraps. In that case, you cannot modify the decorator itself. You have two options: wrap the decorator to add the fix, or apply functools.update_wrapper manually after decoration:
import functools
# A third-party decorator that doesn't use functools.wraps
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# Option 1: Wrap the decorator to add functools.wraps
def fix_decorator(decorator):
"""Wrap a decorator to preserve function metadata."""
@functools.wraps(decorator)
def fixed_decorator(func):
result = decorator(func)
functools.update_wrapper(result, func)
return result
return fixed_decorator
good_decorator = fix_decorator(bad_decorator)
@good_decorator
def my_function():
"""Original docstring."""
return 42
print(my_function.__name__) # my_function
print(my_function.__doc__) # Original docstring.
# Option 2: Manual fix after decoration
@bad_decorator
def another_function():
"""Another docstring."""
return 99
functools.update_wrapper(another_function, another_function.__wrapped__
if hasattr(another_function, '__wrapped__')
else another_function
)
Option 1 is cleaner because it fixes the decorator once and produces a reusable corrected version. Option 2 is a quick patch for cases where you only need to fix one specific function.
Option 2 only works if the third-party decorator's wrapper did not discard the reference to the original function entirely. If the wrapper closes over func but does not expose it through any attribute, there is no way to recover the original metadata without modifying the decorator's source.
Key Takeaways
- Decoration is name rebinding:
@decoratorabovedef func()executesfunc = decorator(func). The namefuncnow points to the wrapper, which has its own__name__,__doc__, and__qualname__. This is why your function reports "wrapper" as its name. functools.wrapsis the one-line fix: Applying@functools.wraps(func)to the wrapper function copies__name__,__doc__,__qualname__,__annotations__,__module__, and__type_params__from the original function and adds a__wrapped__attribute pointing to the original.__wrapped__enables deep introspection:inspect.signature()follows__wrapped__to report the original function's parameter list.inspect.unwrap()follows the entire chain through multiple decorator layers. You can callfunc.__wrapped__()directly to bypass the decorator.__defaults__and__kwdefaults__are not copied by default: If you need these on the wrapper, extend theassignedparameter when callingfunctools.wraps.- Every decorator in a stack needs
functools.wraps: A single decorator that omits it breaks the metadata chain for all decorators above it in the stack. - Class-based decorators use
functools.update_wrapper(self, func): This is the equivalent offunctools.wrapsfor cases where the wrapper is a class instance rather than a function. - In parameterized decorators, apply
@functools.wraps(func)to the innermost wrapper: That is the function that replaces the original and needs to carry its identity.
The "wrapper" name problem is the single point of confusion that causes the overwhelming majority of decorator-related debugging frustration. The fix has been in the standard library since Python 2.5, it costs one import and one line of code, and there is no performance penalty for using it. Every decorator you write should include @functools.wraps(func) on the wrapper function. No exceptions.