How Python Decorators Work Behind the Scenes

Suyash KamathSuyash Kamath
4 min read

Let us understand what do you mean by Decorators first :

A decorator is a special kind of function in Python that:

Takes another function as input, adds extra functionality to it, and returns a new function.

It’s like a wrapper that enhances a function without modifying its code directly.

Why Use Decorators?

  • For logging

  • For timing functions

  • For authorization checks

  • For caching results

  • To make code clean, reusable, and DRY (Don't Repeat Yourself)


Let us understand through an example

Write a decorator that measures the time function takes to execute

Step 1: Read this line

import time

Interpreter loads the built-in time module so we can use time.time() and time.sleep() later.


Step 2: Define the timer() function

def timer(function):
  def wrapper(*args, **kwargs):
    start = time.time()
    result = function(*args, **kwargs)
    end = time.time()
    print(f'Time taken: {end - start}')
    print(f'Result: {result}')
    return result
  return wrapper

Interpreter does not execute this now, it just stores timer as a function object in memory.

  • timer takes one argument: function (this will be the function you're decorating)

  • Inside it, there's a wrapper function defined

  • Wait ! , you might wonder what are these *args and **kwargs arguments

    • *args means: accept any number of positional arguments as a tuple

    • **kwargs means: accept any number of keyword arguments as a dictionary

    • We use them because a decorator might be applied to any function — and we don’t always know how many parameters that function takes!

      So *args and **kwargs allow us to forward any combination of arguments to the original function being decorated.

    • Example :

    • Let’s say you decorate this:

        @timer
        def add(a, b):
            return a + b
      

      When you call:

        add(2, 3)
      

      Here’s what happens inside the decorator:

        # Example 
        wrapper(2, 3)                   # *args = (2, 3), **kwargs = {}
        function(*args, **kwargs)       # function(2, 3)
      
        # let's say wrapper was 
        wrapper(2,3,name=""Suyash")     # *args = (2,3) , **kwargs = {"name"="Suyash"}
      

      ✅ Works perfectly — because we captured and passed the arguments forward.

  • wrapper:

    • Accepts any arguments

    • Calls the original function

    • Measures time

    • Prints duration and result

    • Returns the original result

  • Finally, timer() returns this wrapper

timer is now ready in memory.


Step 3: See the decorator

@timer
def exampleFunction(n):
  time.sleep(n)

This is syntactic sugar for:

def exampleFunction(n):
  time.sleep(n)

exampleFunction = timer(exampleFunction)

Now let’s walk through what happens here:

  1. Python defines exampleFunction(n) as usual (just stores the function in memory).

  2. Then immediately calls:
    exampleFunction = timer(exampleFunction)

    • Passes the exampleFunction to the timer() function as function

    • timer() returns wrapper, which now becomes the new value of exampleFunction

exampleFunction is no longer the original function , it is now wrapper.


Step 4: Call the function

exampleFunction(2)

But remember:

exampleFunction = wrapper

So this is really calling:

wrapper(2)

Now here’s how it executes:

Inside wrapper(2):

start = time.time()

Records the current time in seconds (start)


result = function(*args, **kwargs)

function here is the original exampleFunction, which calls:

time.sleep(2)

So it pauses for 2 seconds


end = time.time()

Records the time after the sleep


print(f'Time taken: {end - start}')

Prints something like:

Time taken: 2.0021

print(f'Result: {result}')

Since the original exampleFunction doesn’t return anything, result = None.

So it prints:

Result: None

return result

Returns None back to the caller.


Final Output:

Time taken: 2.0021
Result: None

Summary of Interpreter Actions

LineInterpreter Action
import timeLoads the time module
def timerStores timer in memory
@timerReplaces exampleFunction with wrapper
exampleFunction(2)Actually calls wrapper(2)
wrapperRuns timing logic, calls original function, prints duration & result

Flow Diagram for Better Understanding :

0
Subscribe to my newsletter

Read articles from Suyash Kamath directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Suyash Kamath
Suyash Kamath