• Home
  • Blog
  • Python
  • Making your Python decorators even better, with functool.wraps

Making your Python decorators even better, with functool.wraps

May 5, 2019 . By Reuven

The good news: I gave a talk on Friday morning, at PyCon 2019, called “Practical decorators.”

The better news: It was a huge crowd, and people have responded very warmly to the talk. Thanks to everyone at PyCon who came to talk to me about it!

However: Several people, at the talk and afterwards, asked me about “functool.wraps“.

So, please think of this post as an addendum to my talk.

Let’s assume that I have the same simple decorator that I showed at the top of my talk, “mydeco”, which takes a function’s output and puts it into a string, followed by three exclamation points:

def mydeco(func):
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    return wrapper

Let’s now decorate two different functions with “mydeco”:

@mydeco
def add(a, b):
'''Add two objects together, the long way'''
return a + b
@mydeco
def mysum(*args):
'''Sum any numbers together, the long way'''
total = 0
for one_item in args:
total += one_item
return total

What happens when I run these functions? They do what we would expect:

>>> add(10, 20)
'30!!!'
>>> mysum(10, 20, 30, 40, 50)
'150!!!

Fantastic! We get each function’s result back, as a string, with the exclamation points. The decorator worked.

But there are a few issues with what we did. For example, what if I ask each function for its name:

>>> add.__name__
'wrapper'
>>> mysum.__name__
'wrapper'

The __name__ attribute, which gives us the name of a function when we define it, now reflects the returned internal function, “wrapper”, in our decorator. Now, this might be true, but it’s not helpful.

It gets even worse if we ask to see the docstring:

>>> help(add)
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
>>> help(mysum)
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)

In other words: We are now getting the docstring and function signature of “wrapper”, the inner function. And this is a problem, because now someone cannot easily find out how our decorated function works.

We can solve this problem, at least partially, by assigning to the __name__ and __doc__ attributes in our decorator:

def mydeco(func):
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    wrapper.__name__ = func.__name__
    wrapper.__doc__ = func.__doc__
    return wrapper

If we use this version of the decorator, then each time we return “wrapper” from our decorator, then we’re doing so after first assigning the original function’s name and docstring to it. If we do this, then things will work the way we want. Mostly:

>>> help(add)
Help on function add in module __main__:

add(*args, **kwargs)
Add two objects together, the long way
>>> help(mysum)
Help on function mysum in module __main__:

mysum(*args, **kwargs)
Sum any numbers together, the long way

The good news is that we’ve now fixed the naming and the docstring problem. But the function signature is still that super-generic one, looking for both *args and **kwargs.

The solution, as people reminded me after my talk, is to use functools.wraps. It’s designed to solve precisely these problems. The irony, of course, is that it might make your head spin a bit more than decorators normally do, because functools.wraps is … a decorator, which takes an argument! Here’s how it looks:

from functools import wraps

def mydeco(func):
    @wraps(func)
    def wrapper(*args, *kwargs):
        return f'{func(*args, **kwargs)}!!!'
    return wrapper

Notice what we’ve done here: We have used the “functool.wraps” decorator to decorate our inner function, “wrapper”. We’ve passed it an argument of “func”, the decorated function passed to “mydeco”. By applying this “wraps” decorator to our inner function, we copy over func’s name, docstring, and signature to our inner function, avoiding the issues that we had seen before:

>>> help(add)
Help on function add in module main:
add(a, b)
Add two objects together, the long way

>>> help(mysum)
Help on function mysum in module main:
mysum(*args)
Sum any numbers together, the long way

So, to answer the questions that I got after my talk: Yes, I would definitely recommend using functool.wraps! It costs you almost nothing (i.e., one line of code), and makes your decorated function work more normally and naturally. And I’m going to try to find a way to squeeze this recommendation into future versions of this talk, as well.

Related Posts

Level up your Python skills this August

Level up your Python skills this August

Prepare yourself for a better career, with my new Python learning memberships

Prepare yourself for a better career, with my new Python learning memberships

I’m banned for life from advertising on Meta. Because I teach Python.

I’m banned for life from advertising on Meta. Because I teach Python.
  • Hello, you have missed a star in the first snippet.

    return f'{func(args, **kwargs)}!!!’ -> return f'{func(*args, **kwargs)}!!!’

    • Good catch; just fixed it now. Thanks!

  • Rafael Gonzalez says:

    it should not be return f'{func(*args, **kwargs)}!!!’ (* in args)

    regards

    • I somehow missed your comment oh-so-long ago. But you’re right; I missed a *. Fixed now!

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >