Create Your First Python Decorator

Learn to write your first Python decorator from scratch, understand the wrapper function pattern, and build a working timing decorator step by step.

8 min read

When you create a Python decorator from scratch, the concept moves from something you use to something you understand. The previous article established that decorators are callables that receive a function and return a replacement, and that the at-syntax is shorthand for passing the defined function through the decorator and rebinding the name. This article turns that knowledge into practice by walking through the construction of a real decorator, starting from the simplest possible wrapper and building up to a working timing decorator that measures how long any function takes to execute.

The core pattern behind every Python decorator is a function that defines another function inside itself and returns that inner function. This inner function is traditionally called the wrapper, and it is the function that actually runs when someone calls the decorated function later. The outer function (the decorator) receives the original function as its argument. The inner function (the wrapper) accepts the arguments that were meant for the original function, does whatever extra work the decorator promises, calls the original function, and returns its result. Understanding this three-layer relationship (decorator receives original, wrapper receives arguments, original receives forwarded arguments) is the key to writing any decorator correctly.

Before you write a decorator that does something useful, it helps to write one that does nothing at all. A null decorator proves that you have the structure right without the distraction of additional logic. It also gives you a template that every subsequent decorator will follow. The null decorator receives a function, defines a wrapper that calls it and returns its result, and returns the wrapper. When you apply this decorator to a function, the function behaves exactly as it did before, which confirms that the wrapping mechanism is working without side effects.

Building the null decorator

The simplest decorator that compiles and runs correctly consists of three parts: the outer function that takes the original function as its parameter, the inner wrapper function that accepts arbitrary arguments, and the return statement that hands the wrapper back. Here is the complete null decorator:

pythonpython
def null_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Walking through this line by line reveals the structure that every decorator you will ever write shares. The first line defines the outer function, taking a single parameter named func. By convention, Python programmers name this parameter func because it will receive a function object, but the name itself has no special meaning. The second line defines the wrapper inside the decorator, which means the wrapper is a closure that has access to func from the enclosing scope. The star-args and double-star-kwargs in the wrapper's parameter list mean it accepts any combination of positional and keyword arguments.

The third line calls the original function with those same arguments, forwarding everything the wrapper received, and returns whatever the original function returns. The final line, which returns the wrapper, is where the decorator hands the replacement function back to Python. Because the at-syntax is equivalent to reassigning the function name to the decorator's return value, whatever the decorator returns becomes the new value bound to the function's name. If the decorator returned something other than a callable, calling the decorated function would fail with a TypeError. If the decorator returned the original function unchanged, the decoration would have no effect. Returning the wrapper is what makes the decoration meaningful.

To test the null decorator, apply it to a simple function and verify that the function still works:

pythonpython
@null_decorator
def add(a, b):
    return a + b
 
result = add(3, 4)
print(result)

When Python processes the line with the at-sign, it calls the null decorator with the add function and rebinds the name to the returned wrapper. When the code later calls add with two numbers, it is actually calling the wrapper, which calls the original function with the same arguments and returns the computed sum. The output is identical to what an undecorated function would produce, which tells you the forwarding logic is correct. If the wrapper forgot to return the result of the original function call, the decorated function would silently return None, and that kind of bug can be difficult to spot without a null decorator test.

Adding behaviour before and after the call

Once the null decorator works, adding real behaviour is a matter of inserting code before the call to the original function, after it, or both. A decorator that logs when a function is called might print a message before calling the original. A decorator that validates arguments might check them before forwarding. A decorator that handles retries might wrap the call in a try-except loop. The wrapper is the right place for all of this logic because it runs every time the decorated function is called, and the original function body runs in the middle, unchanged.

The most instructive first real decorator is a timer. It records the current time before calling the original function, records the time again after the call, and prints the difference. This pattern of bracketing a function call with setup and teardown is the most common decorator structure in practice. Here is a complete timing decorator:

pythonpython
import time
 
def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        duration = end - start
        print(f"{func.__name__} ran in {duration:.4f} seconds")
        return result
    return wrapper

The perf_counter function from the time module is the right choice for measuring short durations because it uses the highest-resolution clock available on the system and is not affected by system clock adjustments. The wrapper stores the start time, calls the original function and saves its return value, captures the end time, computes the elapsed duration, and prints a message that includes the original function's name. The wrapper then returns the saved result so that the decorated function's caller receives the correct return value as if nothing had changed.

Notice that the wrapper captures the return value of the original function call in a variable before printing the timing message. This ordering matters. If the wrapper printed first and then called the original, the timing would not include the print overhead, but the print would appear before the function's own output, which could be confusing. If the wrapper called the original first and then printed, the timing is accurate and the function's output appears in the expected order. The return statement at the end ensures the caller gets the result.

Applying the timer to a real function

With the timer decorator defined, you can measure any function by adding a single line above its definition. Here is an example that computes a sum of squares to give the timer something measurable to report:

pythonpython
@timer
def sum_of_squares(n):
    total = 0
    for i in range(1, n + 1):
        total += i * i
    return total
 
result = sum_of_squares(1_000_000)
print(f"Result: {result}")

When this code runs, the output shows the function name and its duration, followed by the result. The function itself contains no timing code at all. The timing concern is entirely separated into the decorator, and adding timing to another function requires only the at-timer line above its definition. If you later decide to send timing data to a metrics service instead of printing it, you change the decorator once, and every decorated function benefits from the improvement.

The same timer decorator works on functions with any number of arguments because the wrapper uses star-args and double-star-kwargs. It works on functions that return values because the wrapper captures and returns the result. It works on functions that take keyword arguments, default arguments, or no arguments at all. This generality is one of the reasons the star-args and double-star-kwargs pattern appears in almost every decorator wrapper. Without it, you would need a separate version of the timer for every possible function signature, which would defeat the purpose of extracting the timing logic into a reusable decorator.

Understanding what happens at definition time

As the article on Python decorators explained introduced, decorators execute at definition time, not at call time, and this detail deserves a closer look now that you are writing your own. When Python first processes a module and encounters a decorated function definition, it runs the decorator body immediately and replaces the function name with the wrapper. By the time any other code in the module runs, the decorated function is already wrapped. You can verify this by adding a print statement to the decorator body, outside the wrapper:

pythonpython
def timer(func):
    print(f"Decorating {func.__name__}")
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} ran in {end - start:.4f}s")
        return result
    return wrapper

The print statement that announces the decoration runs during module import, once per decorated function, before any calls to those functions occur. This is why decorators cannot depend on runtime state that is not yet available at import time. If your decorator needs to read a configuration file or connect to a database, that initialization must either happen inside the wrapper (so it runs per call) or be guaranteed to succeed at import time. Understanding this distinction prevents subtle bugs where a decorator appears to work during development but fails in a different import order in production.

The definition-time execution also explains why decorator stacking order matters. If you apply both a timer and a null decorator to the same function, the decorator closest to the def line runs first. The result is the same either way when the inner decorator does nothing, but with real decorators that have side effects, the order determines which behaviour wraps which. Later articles cover stacking and parametrized decorators in detail.

The connection to closures

The wrapper function inside a decorator is a closure, and understanding closures in Python helps you debug decorators when they do not behave as expected. A closure is a function that remembers variables from its enclosing scope even after that scope has finished executing. In the timer decorator, the wrapper references func, which is a parameter of the enclosing timer function. When timer returns and its local scope would normally be discarded, the wrapper keeps a reference to func alive. Every time the decorated function is called, the wrapper uses that remembered func to invoke the original.

Closures capture variables by reference, not by value. This matters for decorators that define multiple wrappers in a loop. If a decorator creates wrappers in a for loop and each wrapper references a loop variable, all wrappers will see the final value of that variable unless the variable is captured with a default argument or another closure technique. This is a common source of bugs in decorators that parameterize behaviour.

With a working timing decorator in hand, you have built the foundation for every decorator you will write in the future. The next article on decorating Python functions with arguments addresses a subtle but critical detail: what happens when the function you are decorating takes arguments of its own, and how the wrapper must forward those arguments correctly regardless of their number, type, or combination.

Rune AI

Rune AI

Key Insights

  • A decorator is a function that takes a function, defines an inner wrapper, and returns the wrapper to replace the original.
  • The wrapper function uses *args and **kwargs to accept any arguments and forward them to the original function.
  • The decorator runs at definition time, so the wrapper is in place before any code calls the decorated function.
  • Every real decorator follows the same three-part structure: receive func, define wrapper, return wrapper.
  • Starting with a decorator that does nothing (a null decorator) is the best way to verify your understanding of the pattern before adding real logic.
RunePowered by Rune AI

Frequently Asked Questions

What is the wrapper function in a Python decorator?

The wrapper function is the inner function defined inside a decorator that replaces the original function. It accepts the same arguments as the original (typically using *args and **kwargs), calls the original function inside its body, and can add behaviour before and after that call. The decorator returns the wrapper, and from that point onward, calling the decorated function name actually invokes the wrapper, which in turn invokes the original.

How do I apply the same decorator to multiple functions?

You apply a decorator to a function by placing @decorator_name on the line directly above the function definition. You can place the same @decorator_name above as many function definitions as you like. Each decorated function gets its own independent wrapper, so calls to one decorated function do not interfere with calls to another, even though they share the same decorator code.

Conclusion

Writing your first decorator is the moment where closures, higher-order functions, and the wrapper pattern all come together into a single practical tool. The timing decorator you built in this article is small, but it contains every element that appears in more sophisticated decorators: a function that receives a function, an inner wrapper that adds behaviour, and a return statement that replaces the original. The next step is handling functions that take arguments of their own, which the following article covers in depth.