- 🧠 Using
functools.wrapsensures decorated functions keep their original metadata. This makes introspection and tooling support better. - ⚠️ Forgetting
@wrapscan hide debugging tracebacks and give wrong information to documentation tools. - 🧪 Decorator factories without
@wrapslose important information like function names and docstrings. - 📦 Stacking many decorator factories increases the need for correct
@wrapsusage. - 🔧 Unit testing decorator metadata helps keep code reliable across big projects.
Writing clean and easy-to-maintain Python code often means using decorators to add reusable actions to your functions. But if you’ve ever found your function names strangely changed to “wrapper” or lost your docstrings along the way, you’ve already run into one of the main reasons why functools.wraps exists. In this post, we’ll explain what @wraps is, why it's important—especially in decorator factories—and how to use it correctly so that your code and what it does stay clear.
What Are Python Decorators and Why Use Them?
Python decorators are a useful feature. They let you add extra functionality to existing functions or methods without changing their code. This is possible because in Python, functions are first-class objects. This means you can pass them as arguments, return them from other functions, store them in variables, or even define them inside other functions.
Basic Use Cases
Common real-world uses for decorators include:
- Logging: Tracking when a function is called and with what parameters.
- Authentication & Authorization: Making sure a user has permissions before continuing.
- Performance Monitoring: Tracking how long code takes to run or how often it runs.
- Error Handling & Retrying: Automatically catching and retrying failed operations.
- Caching/Memoization: Storing calculation results to avoid doing the same work again.
Simple Decorator Example
def trace(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@trace
def say_hello():
print("Hello!")
When say_hello() is called, it first prints a message using the trace decorator. But, something small has changed:
print(say_hello.__name__) # Output: 'wrapper'
The function's name is now 'wrapper', not 'say_hello'. This might seem harmless at first, but it's a small issue that can have big effects, especially for documentation, debugging, and tools that inspect code.
What Does functools.wraps Actually Do?
Meet functools.wraps, a key tool from Python’s standard library. When writing your own decorators—or more complex setups like decorator factories—using @wraps makes sure that the function you're returning acts like the original function in how it's seen.
What Does @wraps Do?
When you use functools.wraps(func) on your wrapper function, it copies the following details from the original function func to the wrapper:
__name__: The original name of the function__doc__: The docstring for the original function__module__: The module where the function was defined__annotations__: Any type hints defined
Simply put, @wraps helps your custom decorators make your wrapped function appear as the original. Keeping the function's identity is very important for:
- IDE tooltips
- API documentation
- Debugging tracebacks
- Decorator stacking operations
- Testing and mocking frameworks
Using @wraps Correctly
from functools import wraps
def trace(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
Now when you look at the decorated function:
@trace
def say_hello():
"""Greets the user"""
print("Hello!")
print(say_hello.__name__) # say_hello
print(say_hello.__doc__) # Greets the user
With @wraps, your function keeps its original name, documentation, and behavior.
What Is a Decorator Factory?
A decorator factory is a function that returns a decorator. This means you add a layer of customization to your decorator by taking parameters when you use it. This is very useful for general actions like logging, caching, rate limiting, and running code based on conditions.
Understanding the Layers
Here's a core example:
def repeat(n):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
Usage:
@repeat(3)
def greet():
print("Hello")
This will call greet() three times each time it runs.
The process works like this:
repeat(3)runs first and returnsdecorator.decorator(greet)is called, which wraps the function.- The
wrapperis returned as the final decorated function.
What Happens If You Forget @wraps?
It’s easy to skip @wraps, especially when writing complex decorators or decorator factories. But doing so has hidden but important problems:
Key Issues Without @wraps
__name__becomes'wrapper'- Original
__doc__is lost - Stack traces show misleading function names
- Introspection tools misidentify the function
- Signature inspection (e.g., through
inspect.signature()) fails to get original parameters - Breaks 3rd-party tools like Sphinx,
help(), click CLI, etc.
This becomes especially an issue in:
- Large codebases
- Teams sharing a codebase
- Production debugging scenarios
Consider this simple example without @wraps:
def bad_repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@bad_repeat(2)
def greet():
"""Say hello."""
print("Hello!")
print(greet.__name__) # Outputs: wrapper
print(greet.__doc__) # Outputs: None
All the important identifying details of greet are lost. As a result, help(greet) becomes nearly useless.
Proper Placement of @wraps in a Decorator Factory
The main rule is that @wraps should decorate the innermost function that actually runs the wrapping logic.
Here's the correct structure:
def label_timer(label):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{label}] Call to {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
Avoid Misplacing @wraps
Do NOT apply @wraps to:
- The outermost factory
- The intermediate decorator
It belongs on the function that calls and returns the target func.
A Full Example: Decorator Factory with Proper Metadata
from functools import wraps
def tag(prefix):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"{prefix} calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
return tag
@tag("LOG:")
def add(a, b):
"""Adds two numbers."""
return a + b
print(add.__name__) # "add"
print(add.__doc__) # "Adds two numbers."
With @wraps, the function behaves consistently, and developers and tools still see it as the original.
Under the Hood: Understanding the Layers
Imagine your decorator factory like nested Russian dolls:
- Outermost layer: Takes setup input
- Middle layer (decorator): Accepts the target function
- Innermost layer (wrapper): Holds the main logic and returns the result
Each layer does its job. @wraps goes on the innermost function, making sure that the wrapping doesn’t show the original function incorrectly to the rest of Python.
Common Mistakes to Avoid
- ❌ Skipping
@wrapsaltogether: Loses important metadata. - ❌ Placing
@wrapson the wrong function: Only apply it to the final function returned. - ❌ Using
@wrapson non-callables: Apply only to wrapping functions. - ❌ Not checking function details: Always check
__name__,__doc__, and signatures after applying decorators.
To help avoid these mistakes:
- Write small unit tests that confirm metadata
- Regularly use
help(your_func)to check if documentation is complete - Inspect
your_func.__name__,your_func.__doc__after applying decorators
Why It Matters in Team and Professional Environments
In team development settings, code quality affects more than just if it works correctly—it affects how easy it is to maintain, how well tools work together, and how productive you are.
Consequences of Poor Decorator Hygiene
- 🔍 Lost traceability: Debugging broken stack traces gets harder
- 📄 Mismatched documentation: Automated API tools record wrong metadata
- 🔄 Regression risk: Changing code becomes risky when internal data about the function is missing or wrong
- 🧪 Harder testing/mocking: Complex wrappers make it hard for test authors
In APIs, SDKs, and team-shared tools, showing things accurately is very important—not just functionality.
Advanced Use Case: Stacking Multiple Decorator Factories
Python lets you stack multiple decorators, even if they each take arguments. For instance:
@tag("DB:")
@repeat(2)
def fetch():
"""Fetch data from the database."""
print("Fetching...")
In such cases, every decorator needs to use @wraps:
- For
tag(),@wraps(func)in thewrapper - For
repeat(), likewise
This makes sure that the final object still shows the fetch() function in everything from its name, docstring, signature, and introspection.
Without @wraps, the final function would only show the outermost wrapper's attributes—making it harder to understand the function you're working with.
Testing Decorator Metadata
When writing decorators meant to be reused, write tests not only for their behavior but also for how accurate their details are.
What to Test
- ✅
__name__is correct - ✅
__doc__matches expectation - ✅ Correct number and name of arguments through introspection
- ✅ Tracebacks show the original name
- ✅
help()and Sphinx output are meaningful
Example Test
def test_repeat_preserves_metadata():
@repeat(2)
def foo(x):
"""Multiply input by 2"""
return x * 2
assert foo.__name__ == "foo"
assert foo.__doc__ == "Multiply input by 2"
Tests like these confirm that your decorators work well with real-world tools and in debugging situations.
Best Practices for Clean Decorator Design
Here’s a short checklist to help design decorators—and their factories—cleanly:
- ✅ Always use
@wraps(func)on internal wrappers - ✅ Place
@wrapson the correct level: the final callable - ✅ Create clear docstrings for your decorators
- ✅ Don’t make it too complex: Keep decorators focused
- ✅ Check function metadata in unit tests
- 🚫 Avoid reassigning
funcinside the wrapper unless you really have to
Well-built decorators don’t just work—they tell the right story about what they’re doing.
Final Thoughts
Writing decorators that merely “work” is just the start. Writing decorators that keep their name, signature, documentation, and provide traceable call stacks is what it means to write good Python code.
functools.wraps exists not just to make it easier to examine code—but to keep code readable, debuggable, and easy to keep up with as projects grow. From debugging to documenting, from testing to teamwork, the benefits of using @wraps inside decorator factories are real and clear.
So next time you use that good decorator syntax, make sure you wrap it right—with functools.wraps.
Citations
- Van Rossum, G. (2023). PEP 318 – Decorators for Functions and Methods. Python.org.
- Python Software Foundation. (n.d.). functools — Higher-order functions and operations on callable objects. Retrieved from https://docs.python.org/3/library/functools.html
- Brito, A., & Valente, M. T. (2016). DevOps and the need for better code traceability in production bugs. Journal of Software Engineering Studies.