Why Does My Decorated Function Show wrapper as Its Name

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:

python
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:

python
# 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:

python
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.

python
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.

python
# 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.

python
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.

python
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.

python
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.

python
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.

python
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.

python
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.

Pro Tip

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

  1. Decorators replace functions. The @decorator syntax 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.
  2. 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.
  3. inspect.signature follows __wrapped__. Python's inspect module recognizes the __wrapped__ attribute and uses it to report the original function's signature. This means help(), IDE tooltips, and documentation generators all show the correct parameter list for decorated functions when functools.wraps is used.
  4. 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__.
  5. 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:

python
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.