Python – How to use functools.wraps

There is a little known tool that I wanted to talk about today. It is called wraps and it’s a part of the functools module. You can use wraps as a decorator to fix docstrings and names of decorated functions. Why does this matter? This sounds like a weird edge case at first, but if you’re writing an API or any code that someone other than yourself will be using, then this could be important. The reason being that when you use Python’s introspection to figure out someone else’s code, a decorated function will return the wrong information. Let’s look at a simple example that I have dubbed decorum.py:

# decorum.py
 
#----------------------------------------------------------------------
def another_function(func):
    """
    A function that accepts another function
    """
 
    def wrapper():
        """
        A wrapping function
        """
        val = "The result of %s is %s" % (func(),
                                          eval(func())
                                          )
        return val
    return wrapper
 
#----------------------------------------------------------------------
@another_function
def a_function():
    """A pretty useless function"""
    return "1+1"
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    print a_function.__name__
    print a_function.__doc__

In this code, we decorate the function called a_function with another_function. You can check a_function’s name and docstring by printing them out using the function’s __name__ and __doc__ properties. If you run this example, you’ll get the following for output:


wrapper

A wrapping function

That’s not right! If you run this program in IDLE or the interpreter, it becomes even more obvious how this can get really confusing, really quickly.

>>> import decorum
>>> help(decorum)
Help on module decorum:
 
NAME
    decorum - #----------------------------------------------------------------------
 
FILE
    /home/mike/decorum.py
 
FUNCTIONS
    a_function = wrapper()
        A wrapping function
 
    another_function(func)
        A function that accepts another function
 
>>> help(decorum.a_function)
Help on function other_func in module decorum:
 
wrapper()
    A wrapping function

Basically what is happening here is that the decorator is changing the decorated function’s name and docstring to its own.


Wraps to the Rescue!

How do we fix this little mess? The Python developers have given us the solution in functools.wraps! Let’s check it out:

from functools import wraps
 
#----------------------------------------------------------------------
def another_function(func):
    """
    A function that accepts another function
    """
 
    @wraps(func)
    def wrapper():
        """
        A wrapping function
        """
        val = "The result of %s is %s" % (func(),
                                          eval(func())
                                          )
        return val
    return wrapper
 
#----------------------------------------------------------------------
@another_function
def a_function():
    """A pretty useless function"""
    return "1+1"
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    #a_function()
    print a_function.__name__
    print a_function.__doc__

Here we import wraps from the functools module and use it as a decorator for the nested wrapper function inside of another_function. If you run it this time, the output will have changed:


a_function
A pretty useless function

Now we have the right name and docstring once more. If you go into your Python interpreter, the help function will now work correctly as well. I’ll skip putting it’s output here and leave that for you to try.


Wrapping Up

The wraps decorator is pretty much a one-trick pony, but it’s pretty handy when you need it. If you happen to notice that your functions are not giving you the right name or docstring, then you now know how to fix it pretty easily. Have fun an happy coding!

Print Friendly
  • Diego Alonso Cortez

    Amazing!