The Python wraps Decorator

Introduction

One common challenge with decorators is that they can unintentionally strip away important metadata from the functions they decorate. This metadata includes attributes such as the function’s docstring, name, module, and signature. When a function is decorated, the attributes of the wrapper function, rather than the original function, are exposed to introspection tools like help() or when accessing attributes like __name__.

Losing this metadata can be problematic, especially in scenarios where the original function’s attributes are relied upon for documentation, debugging, or integration with other tools. For instance, if you’re building a library or framework where users need to inspect or interact with functions programmatically, having accurate metadata is crucial for a smooth developer experience.

Fortunately, there is a solution to this problem in the form of the Python wraps decorator, which is available in the functools module. You can use wraps in any decorator you create to preserve the original metadata.

The Problem the Python wraps Decorator Solves

Consider the scenario where you have a decorated function. When you retrieve the function’s docstring using __doc__, or access its __name__ attribute, you might find that the information displayed pertains to the decorator’s inner function rather than the original function.

Example:

def greeting_decorator(fn):
    def decorated(*args, **kwargs):
        ''' This decorator prints a greeting.'''
        print("Hi.")
        return fn(*args, **kwargs)
    return decorated

@greeting_decorator
def introduce_yourself():
    ''' This function prints an introduction.'''
    print("My name is Bobby, I'm 4 years old, and I like trucks.")

print("Function Name:")
print(introduce_yourself.__name__) 
print()

print("Documentation:")
print(introduce_yourself.__doc__)
print()

print("Output:")
introduce_yourself()

Output:

Function Name:
decorated

Documentation:
 This decorator prints a greeting.

Output:
Hi.
My name is Bobby, I'm 4 years old, and I like trucks.

The example above creates a simple decorator (lines 1-6) that prints a greeting prior to the original function’s execution. The function introduce_yourself() (line 9) prints an introduction sentence from a 4-year-old named Bobby (line 11), and it’s decorated by @greeting_decorator (line 8), which adds a printout of “Hi.” Next, the code prints the decorated function’s name using introduce_yourself.__name__ (line 14), its docstring using introduce_yourself.__doc__ (line 18), and finally, executes the function itself (line 22).

As you can see from the output, the function’s name and docstring are those of decorated() (line 2), which is the inner function of the decorator. The function name and docstring print out as “decorated” and “This decorator prints a greeting”, respectively, even though the function is introduce_yourself().

Using the wraps Decorator to Retain Function Metadata

The wraps decorator is straightforward to use. You simply apply @wraps to the inner function that does the decorating within your decorator, giving it the function as an argument. By doing so, you instruct Python to copy the metadata from the original function to the wrapper function.

from functools import wraps

def greeting_decorator(fn):
    @wraps(fn)
    def decorated(*args, **kwargs):
        ''' This decorator prints a greeting.'''
        print("Hi.")
        return fn(*args, **kwargs)
    return decorated

@greeting_decorator
def introduce_yourself():
    ''' This function prints an introduction.'''
    print("My name is Bobby, I'm 4 years old, and I like trucks.")

print("Function Name:")
print(introduce_yourself.__name__) 
print()

print("Documentation:")
print(introduce_yourself.__doc__)
print()

print("Output:")
introduce_yourself()

Output:

Function Name:
introduce_yourself

Documentation:
 This function prints an introduction.

Output:
Hi.
My name is Bobby, I'm 4 years old, and I like trucks.

The example above is identical to the previous once, except that it utilizes the wraps decorator. After importing form functools (line 1), @wraps(fn) is applied to the decorated() function (line 4). This ensures that decorated() retains the docstring, name, and other metadata of the original function, fn.

In the printed output, the function name is now indeed “introduce_yourself”, and docstring is “This function prints an introduction.” Both pieces of metadata are taken from the original introduce_yourself() function.

Here is sample code that summarizes how to use @wraps within a decorator function:

from functools import wraps

def decorator_function(original_function):
    @wraps(original_function)
    def modified_function(*args, **kwargs):
        ... # decorator code

    return modified_function

These are the key elements:

  • from functools import wraps: The import statement that imports the wraps decorator.
  • decorator_function: The decorator function that takes the original function as its argument.
  • original_function: The function to be modified, i.e. decorated.
  • modified_function: The inner function that modifies the original function (a.k.a. wrapper).
  • @wraps(original_function): The call to the @wraps decorator, which decorates modified_function() and makes it preserve the metadata of original_function.

Summary & Reference to the Python wraps Decorator

The wraps decorator is a member of the functools module, and functions to preserve metadata when using decorators. Without it, decorators strip away information from decorated functions, such as their docstrings, names, and signatures.


By employing @wraps, decorators can maintain the original function’s metadata. This is particularly critical for documentation, debugging, or integration with other tools.


Applying @wraps is straightforward. Simply decorate the inner function of your decorator with @wraps and pass the original function as an argument. This instructs Python to copy the metadata from the original function to the function created by the decorator.

from functools import wraps

def decorator_function(original_function):
    @wraps(original_function)
    def modified_function(*args, **kwargs):
        ... # decorator code

    return modified_function