Wrapping a Function Without Rewriting It
Adding logging to ten functions usually means editing ten functions. Write the logging once as a decorator, apply it with one line above each function, and leave the original code untouched.
Worth reading first: Naming a Piece of Work
Adding logging to ten functions usually means editing ten functions. Write the logging once as a decorator, apply it with one line above each function, and leave the original code untouched.
A function that takes a function and returns one
$ >>> def log_calls(func):$ ... def wrapper(*args):$ ... print(f"calling {func.__name__}")$ ... return func(*args)$ ... return wrapper$ ...$ >>> def add(a, b):$ ... return a + b$ ...$ >>> add = log_calls(add)$ >>> add(2, 3)$ calling add$ 5log_calls takes a function in and returns a different function — wrapper — that does the logging, then calls the original underneath. Reassigning add = log_calls(add) replaces the name add with the wrapped version; the original function still exists, just without a name pointing at it anymore.
@ is not special syntax, it is one line saved
$ >>> @log_calls$ ... def add(a, b):$ ... return a + b$ ...$ >>> add(2, 3)$ calling add$ 5@log_calls written above def add(...) does exactly what add = log_calls(add) did on the previous line — Python runs it automatically, immediately after the function is defined.
What a decorator costs you when it goes wrong
Every decorated function now runs through wrapper first, which makes errors inside it harder to trace — a traceback that should point at add often points at wrapper instead, one layer removed from where the real work happens.
add.__name__ reports 'wrapper', not 'add'— the decorator quietly replaced the function's identity along with its behaviour. Adding @functools.wraps(func) above the inner def wrapper copies the original name and docstring back onto it, fixing this without changing what the decorator does.
This is the one line every real decorator in the standard library and popular packages includes, and the one line most hand-written ones forget.
Key takeaways
- A decorator is a function that takes a function and returns a replacement — usually one that wraps the original with extra behaviour.
- @decorator_name above a def is shorthand for func = decorator_name(func), run automatically right after the function is defined.
- A decorated function runs through the wrapper first, which is why tracebacks from decorated code can look one layer removed from the real error.
- functools.wraps(func) on the inner wrapper preserves the original function's name and docstring, which a bare wrapper silently loses.