@functools.wraps(func) is a one-line addition to any custom decorator that preserves the original function's name, docstring, type annotations, and signature. Without it, every decorated function loses its identity and presents itself as the inner wrapper. This guide shows you exactly where to place it in every type of decorator you will write -- simple decorators, parameterized decorators, class-based decorators, stacked chains, and async wrappers -- with copy-ready boilerplate for each.
The Import
Before using @functools.wraps, you need to import the module. There are two common styles:
# Style 1: import the module
import functools
# Usage: @functools.wraps(func)
# Style 2: import the function directly
from functools import wraps
# Usage: @wraps(func)
Both are equivalent. This article uses import functools for clarity so that every usage is explicitly namespaced.
Placement in a Simple Decorator
A simple decorator has two layers: an outer function that receives the target function, and an inner wrapper function that replaces it. The @functools.wraps(func) line goes directly above the def line of the wrapper:
import functools
def log_calls(func): # Outer: receives the function
@functools.wraps(func) # <-- placed here
def wrapper(*args, **kwargs): # Inner: replaces the function
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@log_calls
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
print(add.__name__) # add
print(add.__doc__) # Add two integers.
The rule is mechanical: @functools.wraps(func) goes on whichever function gets returned as the replacement. In a simple decorator, that is always the wrapper.
The Universal Boilerplate
Every simple decorator you write can start from this template. Copy it, rename the decorator, and add your logic in the marked locations:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# YOUR PRE-CALL LOGIC HERE
result = func(*args, **kwargs)
# YOUR POST-CALL LOGIC HERE
return result
return wrapper
Save this boilerplate as a snippet in your editor. Every new decorator you write starts from it. The three things that change each time are the decorator name, the pre-call logic, and the post-call logic. The @functools.wraps(func) line and the *args, **kwargs pattern stay the same every time.
Placement in a Parameterized Decorator
A parameterized decorator -- one that accepts its own configuration arguments like @retry(max_tries=3) -- has three nesting layers. The @functools.wraps(func) still goes on the innermost function:
import functools
import time
def retry(max_tries=3, delay=1.0): # Layer 1: captures config
def decorator(func): # Layer 2: receives the function
@functools.wraps(func) # <-- placed here, on layer 3
def wrapper(*args, **kwargs): # Layer 3: replaces the function
last_exc = None
for attempt in range(1, max_tries + 1):
try:
return func(*args, **kwargs)
except Exception as exc:
last_exc = exc
if attempt < max_tries:
time.sleep(delay)
raise last_exc
return wrapper
return decorator
@retry(max_tries=4, delay=0.5)
def fetch_data(url: str) -> dict:
"""Fetch JSON data from the given URL."""
pass
print(fetch_data.__name__) # fetch_data
print(fetch_data.__doc__) # Fetch JSON data from the given URL.
The argument to @functools.wraps is the func parameter from the layer directly above. In a simple decorator, the outer function receives func and the @wraps argument is func. In a parameterized decorator, the middle function receives func and the @wraps argument is still func. The pattern is always: @wraps receives the function parameter from its enclosing scope.
Placement in a Class-Based Decorator
Class-based decorators use __init__ to receive the function and __call__ to run the wrapper logic. Since there is no standalone wrapper function to decorate with @functools.wraps, you call functools.update_wrapper directly inside __init__:
import functools
class CountCalls:
"""Track how many times a function is called."""
def __init__(self, func):
functools.update_wrapper(self, func) # <-- placed here
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} called {self.count} time(s)")
return self.func(*args, **kwargs)
@CountCalls
def greet(name: str) -> str:
"""Return a personalized greeting."""
return f"Hello, {name}"
print(greet.__name__) # greet
print(greet.__doc__) # Return a personalized greeting.
functools.update_wrapper(self, func) does the same work as @functools.wraps(func). In fact, @functools.wraps(func) is just a convenience decorator that calls update_wrapper internally. The class-based form uses the direct call because there is no function definition to decorate.
Placement in an Async Decorator
Async decorators follow the same placement rule as synchronous ones. The only difference is that the wrapper is defined with async def and uses await:
import functools
import asyncio
import time
def async_timer(func):
@functools.wraps(func) # <-- same placement as sync
async def wrapper(*args, **kwargs):
start = time.perf_counter()
result = await func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@async_timer
async def fetch_user(user_id: int) -> dict:
"""Fetch user data asynchronously."""
await asyncio.sleep(0.1) # Simulating async I/O
return {"id": user_id, "name": "Alice"}
print(fetch_user.__name__) # fetch_user
print(fetch_user.__doc__) # Fetch user data asynchronously.
@functools.wraps(func) works identically on async def functions. It copies the same attributes regardless of whether the wrapper is a coroutine or a regular function.
Placement in Stacked Decorator Chains
When multiple decorators are stacked on the same function, every decorator in the chain needs its own @functools.wraps(func). Each decorator receives a function (which may already be a wrapper from the decorator below) and returns a new wrapper. Each layer must copy metadata from whatever it receives:
import functools
def timer(func):
@functools.wraps(func) # Each decorator needs its own @wraps
def wrapper(*args, **kwargs):
import time
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
return result
return wrapper
def log_calls(func):
@functools.wraps(func) # Each decorator needs its own @wraps
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
@timer
def multiply(x: int, y: int) -> int:
"""Multiply two integers."""
return x * y
print(multiply.__name__) # multiply
print(multiply.__doc__) # Multiply two integers.
If timer uses @wraps but log_calls does not, the metadata breaks at log_calls. If log_calls uses @wraps but timer does not, the metadata breaks at timer and log_calls copies the broken metadata from timer's wrapper. Every link in the chain must participate for the original metadata to survive to the outermost layer.
If a third-party decorator in your stack does not use @functools.wraps, it will break metadata propagation for the entire chain above it. You cannot fix this from outside the decorator without replacing or wrapping it.
When You Do Not Need @wraps
There is one category of decorator where @functools.wraps is unnecessary: decorators that return the original function unmodified. A registration decorator, for example, stores a reference to the function and then returns it without wrapping it in a new function:
REGISTRY = {}
def register(func):
"""Register a function in the global registry."""
REGISTRY[func.__name__] = func
return func # Returns the ORIGINAL function, not a wrapper
@register
def process():
"""Process data."""
pass
print(process.__name__) # process (no wrapper involved, no @wraps needed)
Because the original function is returned unchanged, there is no wrapper to copy metadata onto. The function's identity is never replaced. This is the only case where @functools.wraps is not needed. If a decorator returns a new function object, it needs @wraps.
Common Placement Mistakes
Missing parentheses: @functools.wraps instead of @functools.wraps(func)
# WRONG -- passes the wrapper to wraps as the "wrapped" argument
def my_decorator(func):
@functools.wraps # Missing (func) !
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# RIGHT
def my_decorator(func):
@functools.wraps(func) # Correct
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
Without the parentheses, functools.wraps receives the wrapper function itself as its argument instead of the original function. This produces incorrect behavior -- the wrapper ends up copying metadata from itself.
Placed on the wrong layer in a parameterized decorator
# WRONG -- @wraps on the middle layer
def repeat(n):
@functools.wraps # Wrong layer, and missing (func)
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
# RIGHT -- @wraps on the innermost layer
def repeat(n):
def decorator(func):
@functools.wraps(func) # Correct layer and correct argument
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
Applied to the decorator function instead of the wrapper
# WRONG -- @wraps on the outer function
@functools.wraps # This makes no sense here
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# RIGHT -- @wraps on the wrapper
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
Verifying Correct Placement
After adding @functools.wraps(func) to your decorator, verify with this four-line check:
import inspect
@my_decorator
def sample(x: int, y: int = 10) -> int:
"""A sample function for verification."""
return x + y
assert sample.__name__ == "sample", f"Name wrong: {sample.__name__}"
assert sample.__doc__ == "A sample function for verification.", f"Doc wrong: {sample.__doc__}"
assert hasattr(sample, '__wrapped__'), "__wrapped__ missing"
assert str(inspect.signature(sample)) == "(x: int, y: int = 10) -> int", "Signature wrong"
print("All metadata preserved correctly.")
If any assertion fails, the decorator is not using @functools.wraps correctly. The four checks cover the function name, docstring, __wrapped__ attribute presence, and the complete parameter signature including type annotations and defaults.
Key Takeaways
- Import first:
import functools. Thewrapsfunction lives in thefunctoolsstandard library module. No installation is needed -- it ships with Python. - Place
@functools.wraps(func)directly above the wrapperdefline. The wrapper is the innermost function that replaces the original in the namespace. This is true for simple, parameterized, and async decorators alike. - The argument is always the function from the enclosing scope. In a simple decorator, the outer function receives
func. In a parameterized decorator, the middle function receivesfunc. The@wrapsargument is always thatfuncparameter. - For class-based decorators, call
functools.update_wrapper(self, func)in__init__. This is the explicit-call form of@wrapsand works the same way on class instances. - Every decorator in a stacked chain needs its own
@wraps. Metadata propagation is a chain -- each decorator copies metadata from what it receives, so every link must participate. - Skip
@wrapsonly when returning the original function unmodified. Registration decorators that store a reference and return the function as-is do not create a wrapper, so there is nothing to copy metadata onto. - Verify with four assertions. Check
__name__,__doc__,hasattr(__wrapped__), andinspect.signature(). If all four pass, the decorator is correctly preserving metadata.
The placement rule for @functools.wraps(func) is consistent across every decorator variant: it goes on the function that replaces the original. Once that rule is internalized, applying it becomes automatic. Every decorator template in this article -- simple, parameterized, class-based, async, and stacked -- follows the exact same pattern with the exact same result: the decorated function keeps its original identity.