Execute Program

Python in Detail: functools.wraps

Welcome to the functools.wraps lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • Most decorators build and return new functions. There's a subtle problem with that: what happens to the original function's attributes, like .__name__?

  • >
    def divide(a, b):
    return a / b

    divide.__name__
    Result:
    'divide'Pass Icon
  • If our decorator returns a function defined with def wrapped, then the final decorated function's .__name__ is "wrapped". We lose the original function's name.

  • Here's the silence_exceptions decorator from some previous lessons.

  • >
    def silence_exceptions(func):
    def wrapped(*args, **kwargs):
    try:
    return func(*args, **kwargs)
    except Exception:
    return None

    return wrapped
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    @silence_exceptions
    def divide(a, b):
    return a / b

    divide.__name__
    Result:
    'wrapped'Pass Icon
  • Imagine a system where we decorate hundreds of functions. They all get the name "wrapped". In our generated documentation and logs, they all show up as "wrapped" instead of their real names! We can't tell which function is which.

  • We can manually fix the name when we decorate the function. In this example, note the new wrapped.__name__ = line.

  • >
    def silence_exceptions(func):
    def wrapped(*args, **kwargs):
    try:
    return func(*args, **kwargs)
    except Exception:
    return None

    wrapped.__name__ = func.__name__
    return wrapped
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    @silence_exceptions
    def divide(a, b):
    return a / b

    divide.__name__
    Result:
    'divide'Pass Icon
  • That works, but there's a problem with it: .__name__ isn't the only attribute that we need to copy! We also need to retain .__doc__, the function's docstring. There are more esoteric attributes to copy as well: .__qualname__, .__module__, .__annotations__, and .__type_params__. Future versions of Python may add even more attributes that we should copy.

  • Think for a moment about the shape of this problem: "we need to change many decorator functions in the same way". Python already has a way to change many functions in the same way: decorators!

  • The built-in functools.wraps decorator copies all of these special attributes from the original function to the decorated wrapper. It's a decorator that we use on decorators! If future versions of Python add more attributes that we should copy, functools.wraps will copy those as well.

  • Pay close attention to the @functools.wraps(func) line in the next example.

  • >
    import functools

    def silence_exceptions(func):
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
    try:
    return func(*args, **kwargs)
    except Exception:
    return None

    return wrapped
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    @silence_exceptions
    def divide(a, b):
    return a / b

    divide.__name__
    Result:
    'divide'Pass Icon
  • You might wonder: shouldn't Python do this for us automatically? It knows that we're decorating a function, right?

  • No, it doesn't know that! Decorators usually return functions. But they sometimes return other values too, as we saw in an earlier lesson. For example, here's a decorator that returns integers.

  • >
    def replace_with_55(f):
    return 55

    @replace_with_55
    def divide(x, y):
    return x / y

    divide
    Result:
    55Pass Icon
  • The @some_decorator syntax does two things. First, it calls some_decorator on the decorated value. Second, it replaces the original value with the decorator's return value.

  • Python can't make any assumptions beyond those two things. If we want anything extra, like copying a function's .__name__, then we have to ask for it.