runtime
163 / 250
lesson 163 Decorators & Context Managers · advanced

functools.wraps: keep the function's identity

A plain wrapper hides the function it wraps: its name becomes wrapper and its docstring disappears. Logs, error messages, help(), test runners and registries that look functions up by name all get confused. functools.wraps copies the original's identity onto the wrapper.

You'll be able to
  • Explain what a wrapper loses: __name__, __doc__, __qualname__, __module__
  • Apply @functools.wraps(func) to the wrapper inside a decorator
  • Use __wrapped__ to reach the original, for testing or introspection
The idea

To the rest of the program, a decorated function *is* its wrapper. So without help, parse_depth.__name__ says wrapper, the docstring is gone, and every decorated function looks the same in a log or a traceback. `functools.wraps(func)` is itself a decorator (with an argument!) that you put on the wrapper: it copies __name__, __qualname__, __doc__, __module__ and the function's attribute dictionary from func, and stores the original in wrapper.__wrapped__.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Before and after wraps
import functools

def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

def careful(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

def parse_depth(text):
    """Depth in km from the USGS text value."""
    return float(text)

a = plain(parse_depth)
b = careful(parse_depth)
print(a.__name__, a.__doc__)
print(b.__name__, b.__doc__)
print(b.__wrapped__ is parse_depth)

It matters most when code finds functions by name. This event-handler registry stores functions under __name__. With plain wrappers both handlers register as wrapper, and the second silently replaces the first.

example-2.py · A registry keyed by name
import functools

def counted(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

handlers = {}

def register(func):
    handlers[func.__name__] = func
    return func

@register
@counted
def on_push(event):
    return f"push to {event['repo']}"

@register
@counted
def on_watch(event):
    return f"star on {event['repo']}"

print(sorted(handlers))
print(handlers["on_watch"]({"repo": "tidewater/smartqueue"}), handlers["on_watch"].calls)
print(handlers["on_push"].__wrapped__({"repo": "cobalt-tools/deeplint"}), handlers["on_push"].calls)

When to use it: in every wrapper you write; it costs one line. Mistakes to avoid: putting @functools.wraps on the decorator instead of on the wrapper; writing @functools.wraps without (func); and setting attributes on the wrapper *before* wraps runs, because wraps copies the original's attribute dictionary over the wrapper's.

Review
  • A plain wrapper replaces the function's name and docstring with its own.
  • @functools.wraps(func) on the wrapper copies the identity and adds __wrapped__.
  • Stacked decorators apply bottom-up; __wrapped__ leads back down to the original.
  • Next: context managers, the other way to put code around something: a block, with with.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

functools.wraps: keep the function's identity — Decorators & Context Managers · Runtime