Part 5 of 6 · Chapter 4 of 4

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.

Advanced11 min read

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

Terminal
$ >>> 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$ 5

log_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

Terminal
$ >>> @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.

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.