What this usually means
This is the classic 'decorator pattern without functools.wraps' bug. When you write a decorator, you typically define an inner function (the wrapper) that replaces the original function. Python assigns the wrapper's metadata (__name__, __doc__, __module__, __annotations__, etc.) to the decorated object. If you don't copy the original function's attributes to the wrapper, every consumer of that function—like help(), logging, serialization, or documentation generators—sees the wrapper's data instead of the original. The root cause is almost always that the decorator returns wrapper without calling @functools.wraps(func) or manually reassigning the attributes.
The first ten minutes — establish facts before touching code.
- 1Print the decorated function's __name__ attribute: print(decorated_func.__name__). If it's not the original name, you've hit this bug.
- 2Check if the decorator uses @functools.wraps. If not, that's the likely cause.
- 3Inspect the decorator's code: look for a nested function that returns wrapper without wrapping it.
- 4For a decorator with arguments, verify that the outer decorator factory correctly applies wraps to the innermost wrapper.
- 5Use inspect.unwrap(decorated_func) to see if the original function is accessible via __wrapped__ attribute.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchThe decorator definition file (likely where the wrapper function is defined).
- searchThe decorated function's __name__, __doc__, and __module__ attributes.
- searchThe functools.wraps documentation and source code to understand what it copies.
- searchAny third-party decorator library (like Flask's @app.route or Django's @login_required) that might have its own wrapping logic.
- searchThe output of help(decorated_func) in an interactive shell.
- searchThe decorator's return statement: it should return wrapper (possibly wrapped) not the original function.
Practical causes, not theory. These are the things you will actually find.
- warningDecorator defined without @functools.wraps (most common).
- warningUsing a decorator class that returns an instance without delegating __name__ etc. to the wrapped function.
- warningMultiple decorators stacked in wrong order, causing the outer decorator to lose metadata that the inner one preserved.
- warningCustom decorator that manually copies some attributes (like __name__) but misses others (like __wrapped__ or __qualname__).
- warningDecorator that returns a different callable (e.g., a lambda) without copying attributes.
- warningUsing inspect.getmembers or type() checks that rely on __name__ and fail silently.
Concrete fix directions. Pick the one that matches your root cause.
- buildAdd @functools.wraps(func) to the wrapper function inside the decorator. This copies __module__, __name__, __qualname__, __annotations__, __doc__, and creates __wrapped__ attribute.
- buildIf you need to preserve additional attributes, use functools.update_wrapper(wrapper, func, assigned=functools.WRAPPER_ASSIGNMENTS + ('extra_attr',), updated=functools.WRAPPER_UPDATES + ('__dict__',)).
- buildFor class-based decorators that implement __call__, use functools.update_wrapper(self, func) in __init__.
- buildWhen stacking decorators, ensure the innermost decorator applies wraps first, and outer ones either preserve __wrapped__ or use wraps themselves.
- buildUse a decorator factory pattern where the inner decorator applies wraps before returning the wrapper.
A fix you cannot prove is a guess. Close the loop.
- verifiedRun print(decorated_func.__name__) — should now be the original function name.
- verifiedRun help(decorated_func) — should show the original function's signature and docstring.
- verifiedCheck that decorated_func.__wrapped__ exists and equals the original function.
- verifiedRun unit tests that check function metadata — they should pass.
- verifiedUse inspect.signature(decorated_func) and confirm it matches the original signature.
- verifiedVerify logging output shows the correct function name when using the decorated function in a logger.
Things that make this bug worse or harder to find.
- warningManually copying only __name__ and forgetting __doc__ or __module__.
- warningApplying @functools.wraps to the wrong function (e.g., to the outer decorator factory instead of the wrapper).
- warningUsing @functools.wraps on a function that is not the wrapper (e.g., on the original function before returning it).
- warningForgetting that decorators with arguments require an extra layer; wraps must be applied at the innermost wrapper.
- warningAssuming that all third-party decorators use wraps correctly (they often don't).
- warningUsing sys.setprofile or trace functions that rely on function names and breaking them silently.
Flask route decorator silently breaks logging function names
Timeline
- 10:15Deploy new endpoint /api/v2/orders with a custom decorator @validate_payload.
- 11:00Sentry alerts show all logs from the new endpoint labeled as 'wrapper', not 'create_order'.
- 11:05Run help(create_order) in Flask shell — docstring shows 'Validates payload' (the decorator's doc) instead of the actual endpoint doc.
- 11:10Inspect create_order.__name__ — returns 'wrapper'.
- 11:12Check the @validate_payload decorator source code: no @functools.wraps present.
- 11:15Add @functools.wraps(func) to the inner wrapper function.
- 11:20Redeploy — logs now show correct function name 'create_order'.
- 11:25Verify help(create_order) shows correct docstring and signature.
I was shipping a new API endpoint for order creation. The endpoint had a decorator @validate_payload that checked JSON schema. I'd written that decorator months ago and never bothered with functools.wraps because it worked fine functionally. But once we deployed, Sentry started grouping all errors under 'wrapper' instead of the actual endpoint function name. Our logging pipeline uses Loguru which relies on __name__ to tag log entries. Every log from the new endpoint showed 'wrapper' as the function name, making debugging impossible.
I SSHed into a staging box and ran a quick Python shell. I imported the endpoint function and printed its __name__ — sure enough, it was 'wrapper'. help() showed the decorator's inner docstring. I opened the decorator file and immediately saw the problem: def wrapper(*args, **kwargs): return func(*args, **kwargs) with no @wraps. The fix was a single line: @functools.wraps(func) above the wrapper definition.
After redeploying, I verified that __name__, __doc__, and __wrapped__ were all correct. The logs started showing 'create_order' again. I also discovered that the decorator was used in 12 other places, all suffering the same issue. I fixed them all in one commit. The lesson: never write a decorator without functools.wraps. It's a two-second addition that prevents hours of debugging.
Root cause
The @validate_payload decorator omitted @functools.wraps, causing the decorated function to inherit the wrapper's metadata (__name__='wrapper', __doc__=None).
The fix
Added @functools.wraps(func) to the inner wrapper function inside the decorator.
The lesson
Always apply @functools.wraps in custom decorators to preserve the original function's metadata. It's a one-line fix that prevents broken logging, introspection, and documentation.
When you apply @functools.wraps(func) to the wrapper function, it copies the following attributes from func to wrapper: __module__, __name__, __qualname__, __annotations__, __doc__, and the __dict__ attribute (update). It also sets a new attribute __wrapped__ on wrapper pointing to func. This allows tools like inspect.unwrap() to retrieve the original function.
The assignment list is stored in functools.WRAPPER_ASSIGNMENTS: ('__module__', '__name__', '__qualname__', '__annotations__', '__doc__'). The update list is functools.WRAPPER_UPDATES: ('__dict__',). If you need to preserve additional attributes (like __defaults__ or __kwdefaults__), you must extend these lists manually via update_wrapper().
Decorators implemented as classes (with __call__) suffer the same metadata loss if they don't delegate attribute access to the wrapped function. The class instance's __name__ will be the class name, not the function's. To fix: in __init__, call functools.update_wrapper(self, func). This copies the relevant attributes from func to the instance. Additionally, override __getattr__ to fallback to func for attributes not found on the instance.
Example: class MyDecorator: def __init__(self, func): functools.update_wrapper(self, func); self.func = func. But note: __name__ is set on the instance, not the class, so it works. However, some introspection tools (like inspect.isfunction) will return False for class instances, which may cause other issues. Prefer function-based decorators with wraps for maximum compatibility.
When a decorator takes arguments, you have three nested functions: the outer factory, the middle decorator, and the inner wrapper. The @wraps must be applied to the innermost wrapper, not the middle one. A common mistake is applying @wraps to the middle decorator function, which does nothing. Example: def repeat(n): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): ... ; return wrapper; return decorator. The @wraps goes on wrapper, not on decorator.
If you accidentally put @wraps on decorator, you'll copy func's attributes to decorator (which returns wrapper), but the returned wrapper still lacks the attributes. Always verify by checking __name__ after the decorator is applied.
When stacking decorators, each decorator that uses wraps sets __wrapped__ on its wrapper. This creates a chain: outer.__wrapped__ -> inner.__wrapped__ -> original. The inspect.unwrap() function follows this chain until it finds an object without __wrapped__. If any decorator in the chain does not use wraps, the chain breaks and unwrap may return the wrong object or fail.
To preserve the chain, ensure every decorator in the stack applies wraps. Also, if you manually set __wrapped__, make sure it points to the original function, not an intermediate wrapper, unless you want to preserve the chain. The standard wraps sets __wrapped__ to the immediate wrapped function, which is correct for a linear chain.
Write a simple unit test that checks the decorated function's __name__ and __doc__. For example: def test_decorator_preserves_metadata(): @my_decorator def foo(): '''Docstring''' ; pass ; assert foo.__name__ == 'foo'; assert foo.__doc__ == 'Docstring'. This test will catch missing wraps immediately.
Also test that inspect.signature works and that __wrapped__ exists and is the original function. Use unittest.mock.patch to simulate a decorator that doesn't use wraps and verify your code handles it gracefully.
Frequently asked questions
Why does my decorator work fine functionally but break logging and documentation?
The decorator runs the original function correctly, but the wrapper function's metadata (__name__, __doc__) replaces the original's. Logging frameworks often use __name__ to tag entries, and documentation tools use __doc__ and signatures. Without functools.wraps, these tools see the wrapper's metadata instead of the original function's.
Can I manually copy attributes instead of using functools.wraps?
Yes, but it's error-prone. You'd need to copy all attributes from functools.WRAPPER_ASSIGNMENTS and set __wrapped__. For example: wrapper.__name__ = func.__name__; wrapper.__doc__ = func.__doc__; etc. The functools.wraps decorator does this automatically and also handles edge cases like partial objects. Manual copying is not recommended unless you need custom behavior.
Does @functools.wraps affect performance?
Minimally. It only runs at decoration time (once per decorated function) and copies a few attributes. The runtime overhead of accessing __name__ on the wrapper is unchanged. In benchmarks, the difference is negligible. The benefits of correct metadata far outweigh any micro-performance cost.
What if my decorator returns a lambda or a different callable?
You can still use update_wrapper on the lambda. For example: return functools.update_wrapper(lambda *args, **kwargs: func(*args, **kwargs), func). However, lambdas have limitations (no annotations, no docstring). Consider using a named inner function instead for better metadata preservation.
How do I debug a decorator that is losing metadata when it's from a third-party library?
You can monkey-patch the decorated function after import: from third_party import decorated_func; decorated_func.__name__ = 'original_name'. But this is brittle. Better to wrap the third-party decorator with your own that applies wraps: from functools import wraps; def my_decorator(func): return wraps(func)(third_party_decorator(func)). Then use @my_decorator.