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.
- 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
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.
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.
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.
- 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.