Introduction
When you decorate a function, the wrapper replaces the original in every way -- including its name, docstring, and type annotations. This causes real problems for debugging, documentation generation, and introspection tools. The functools.wraps decorator solves this by copying the original function's metadata onto the wrapper, and Python 3.14 brings important changes to how annotations are handled.
Key Concepts
functools.wraps(func)-- A decorator applied to the wrapper function that copies key metadata attributes from the originalfunconto the wrapper.__wrapped__-- A special attribute set byfunctools.wrapsthat stores a reference to the original unwrapped function.__annotate__-- The annotation evaluator function introduced by PEP 649 in Python 3.14, which lazily produces__annotations__on demand.__type_params__-- Type parameter metadata for generic functions, available since Python 3.12 (PEP 695).
Real World Context
In production code, broken metadata is more than a cosmetic issue. Documentation generators like Sphinx rely on __name__ and __doc__ to produce API docs. Debugging tools and profilers display __name__ and __qualname__ in stack traces. Serialization frameworks like Pickle use __qualname__ to locate functions. Type checkers and IDE tooling inspect annotations. Without functools.wraps, all of these tools see the wrapper's metadata instead of the original function's, leading to confusing docs, misleading stack traces, and broken serialization.
Deep Dive
To understand the problem, consider a simple decorator that does not use functools.wraps:
pythondef my_decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @my_decorator def greet(name): """Greet someone.""" return f"Hello, {name}" print(greet.__name__) # "wrapper" - Not "greet"! print(greet.__doc__) # None - Docstring lost!
After decoration, greet is actually wrapper. Its __name__ is "wrapper", its docstring is None, and any annotations are gone. This is because the decorator returned a brand-new function object that has no knowledge of the original.
The fix is to apply @functools.wraps(func) to the wrapper, which copies all the relevant metadata:
pythonimport functools def my_decorator(func): @functools.wraps(func) # Copy metadata from func to wrapper def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @my_decorator def greet(name): """Greet someone.""" return f"Hello, {name}" print(greet.__name__) # "greet" - Correct! print(greet.__doc__) # "Greet someone." - Preserved!
Now greet.__name__ is "greet", the docstring is preserved, and greet.__wrapped__ gives access to the original unwrapped function.
functools.wraps copies the following attributes from the original function to the wrapper:
__module__: Module name__name__: Function name__qualname__: Qualified name__annotations__: Type annotations dictionary__type_params__: Type parameters (since Python 3.12, PEP 695)__doc__: Docstring
It also updates __dict__ (merging function attributes) and sets __wrapped__ to the original function. In Python 3.14, __annotate__ (PEP 649) is also copied internally if present.
A notable change in Python 3.14 is PEP 649 (deferred evaluation of annotations). Python 3.14 adds __annotate__ -- a callable that lazily evaluates annotations on demand. functools.wraps copies __annotate__ alongside __annotations__ when present. This means annotations are no longer eagerly evaluated at function definition time, improving startup performance and eliminating forward-reference issues. Accessing __annotations__ on wrapped functions still works because Python 3.14 generates it on the fly from __annotate__.
Similarly, since Python 3.12, functools.wraps also copies __type_params__, ensuring that generic function type parameters (defined with the type statement or TypeVar) are preserved through decoration.
Common Pitfalls
- Forgetting
functools.wrapsentirely. This is the most common mistake. Every decorator you write should use@functools.wraps(func)on its wrapper unless you have a specific reason not to. - Assuming
__wrapped__is always the true original. If multiple decorators are stacked, each one sets__wrapped__to its immediate input -- not the bottom-most original. To unwrap fully, useinspect.unwrap(func), which follows the__wrapped__chain. - Mutating
__annotations__directly on Python 3.14+. While reading__annotations__still works (it is generated from__annotate__on demand), directly mutating__annotations__may not behave as expected with deferred evaluation. Useannotationlib.get_annotations()for reliable access.
Best Practices
- Always apply
@functools.wraps(func)to your wrapper function. It costs nothing and prevents a wide class of metadata-related bugs. - Use
inspect.unwrap(func)when you need to access the true original function through multiple layers of decoration. - When writing class-based decorators, use
functools.update_wrapper(self, func)in__init__to achieve the same metadata copying.
Summary
- Without
functools.wraps, decorated functions lose their name, docstring, and annotations. @functools.wraps(func)copies all key metadata from the original function to the wrapper.- In Python 3.14,
functools.wrapsalso copies__annotate__(PEP 649) alongside__annotations__, supporting deferred annotation evaluation. - Since Python 3.12,
__type_params__is also preserved for generic functions (PEP 695). - The
__wrapped__attribute provides access to the original function for introspection and unwrapping.
Code Examples
import functools
def debug(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@debug
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
# Metadata preserved
print(add.__name__) # add
print(add.__doc__) # Add two numbers.
print(add.__wrapped__) # <function add at 0x...> (original function)