keepcalculator

06 Numbers and Powers

Python Decorators: How They Wrap Functions

Card No. 06·64158 · Updated · Standard formula, shown below

Python decorators transform a function by wrapping it in another function, letting you add behavior before, after, or around the original call without changing its source code. You apply a decorator with the @name syntax above a function definition, and the result is a new function that the interpreter binds to the original name.

What a Decorator Actually Does

At its core, a decorator is just a callable (usually a function) that takes a function as an argument and returns a new function. When you write:

@timer
def fetch_data():
    ...

Python translates it to:

def fetch_data():
    ...
fetch_data = timer(fetch_data)

The name fetch_data now points to whatever timer returned, not the original function. That returned object is the wrapper — it typically calls the original function inside its own logic.

Anatomy of a Wrapper

A minimal wrapper looks like this:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before the call")
        result = func(*args, **kwargs)
        print("After the call")
        return result
    return wrapper

The wrapper accepts any positional and keyword arguments (*args, **kwargs) so it can forward them to the original function. It captures the return value, does something extra, then returns that value. Without the return result, the decorated function would silently return None.

Preserving Function Metadata

One common gotcha: the wrapper replaces the original function, so its name, docstring, and signature are lost. This breaks debugging and tools that rely on introspection. The standard fix is functools.wraps:

from functools import wraps

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

@wraps copies __name__, __doc__, and other attributes from the original to the wrapper. It also updates __wrapped__, which lets some tools see through the decorator. Always use it unless you have a reason not to.

Decorators with Arguments

Sometimes you need to pass options to a decorator, like @retry(times=3). That requires a third level of nesting:

def retry(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    pass
            raise
        return wrapper
    return decorator

Now @retry(times=3) is equivalent to func = retry(times=3)(func). The outer function receives the argument, the middle one receives the function, and the inner one handles the call.

Common Uses and Examples

Decorators shine for cross-cutting concerns that apply to many functions:

  • Logging — record when a function is called and its arguments.
  • Timing — measure execution time, useful for profiling.
  • Caching — store results for identical inputs (see functools.lru_cache).
  • Access control — check permissions before running a function.
  • Input validation — verify arguments before the body runs.

For example, a simple timing decorator:

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

Apply it with @timer above any function, and every call prints the duration.

Stacking Decorators

You can apply multiple decorators, and they execute bottom-up. For:

@a
@b
def f(): ...

Python applies b first, then a to the result: f = a(b(f)). The order matters. A common pattern is to put @wraps innermost (closest to the function) so it copies metadata from the original, not from an intermediate wrapper.

Frequently Asked Questions

Can a decorator change the arguments or return value?

Yes. The wrapper can inspect, modify, or replace the arguments before calling the original, and can alter or substitute the return value. This is how validation and transformation decorators work.

How do I make a decorator that works on methods?

Methods are just functions whose first argument is self (or cls). A normal decorator works fine — just remember to accept *args, **kwargs so self is passed through. For classmethods or staticmethods, apply the decorator after @classmethod or @staticmethod.

What is the difference between a decorator and a context manager?

A decorator wraps a function definition and controls when it is called. A context manager (used with with) wraps a block of code and controls setup/teardown around it. They solve different problems, though both add behavior without modifying the original code.

Why do I see “missing 1 required positional argument” when using a decorator?

That usually means the wrapper doesn’t accept and forward *args, **kwargs, or the decorator itself is missing a level of nesting when it takes arguments. Check that the innermost wrapper accepts arbitrary arguments and passes them to the original function.

Can I decorate a class?

Yes. A decorator can take a class and return a new class or a modified version of it. This is often used for registering classes, adding methods, or applying metaclass-like behavior.

Does the order of multiple decorators matter?

Yes, because they apply bottom-up. The decorator closest to the function runs first, and its result is passed to the one above. Changing the order can change behavior, especially with side effects like logging or caching.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →