All Courses
Advanced Python

Python Decorators

What is a Decorator?

A decorator is a function that wraps another function to add behaviour before and/or after it runs — without modifying the original function's code.

We've already seen built-in decorators: - @property — makes a method behave like an attribute - @staticmethod — marks a method as static - @classmethod — marks a method as a class method


How a Decorator Works

A decorator uses a nested (closure) function called a wrapper:

def my_decorator(func):          # takes a function as argument
    def wrapper(*args, **kwargs):
        print("Before the function")
        result = func(*args, **kwargs)   # call the original function
        print("After the function")
        return result            # always return the result!
    return wrapper               # return the wrapper, not the result

Using the decorator syntax

@my_decorator
def foo(x, y):
    return x + y

What @my_decorator actually does

# These two are exactly equivalent:
@my_decorator
def foo(x, y): ...

foo = my_decorator(foo)   # explicit version

The decorator replaces foo with the wrapper function. When you call foo(1, 2), you're actually calling wrapper(1, 2).


Critical Rules for Writing Decorators

1. Use *args and **kwargs in the wrapper

Without them, the decorator only works for functions with a specific number of parameters:

# ❌ Only works for functions with exactly one argument
def wrapper(x):
    result = func(x)
    return result

# ✅ Works for any function with any parameters
def wrapper(*args, **kwargs):
    result = func(*args, **kwargs)
    return result

2. Always return the result

If you don't return the result, the decorated function silently returns None:

# ❌ Loses the return value
def wrapper(*args, **kwargs):
    func(*args, **kwargs)

# ✅ Preserves the return value
def wrapper(*args, **kwargs):
    result = func(*args, **kwargs)
    return result

Full Example — Timer Decorator

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        total_time = end_time - start_time
        print("Time taken to execute:", total_time)
        return result
    return wrapper


@timer
def loop():
    for _ in range(1_000_000):   # _ is an anonymous variable — value not needed
        pass

@timer
def get_max(x, y, z):
    return max(x, y, z)

loop()          # Time taken to execute: 0.05...
get_max(1,2,3)  # Time taken to execute: 0.000...  → 3

_ as a loop variable is a Python convention meaning "I need a variable here but won't use it."


Multiple Decorators

You can stack decorators — the one closest to the function is applied first:

def pretty_printer(func):
    def wrapper(*args, **kwargs):
        print()                         # blank line before
        result = func(*args, **kwargs)
        print()                         # blank line after
        return result
    return wrapper


@timer
@pretty_printer
def print_numbers(num):
    for i in range(num):
        print(i)

What stacking actually means

# @timer above @pretty_printer is equivalent to:
print_numbers = timer(pretty_printer(print_numbers))

Order of execution: 1. pretty_printer wraps print_numbers first → produces a wrapper 2. timer then wraps that wrapper 3. When called: timer's wrapper runs → calls pretty_printer's wrapper → calls print_numbers

So the output order is:

[blank line from pretty_printer — before]
[blank line from pretty_printer — before]  ← timer calls pretty_printer's wrapper
... function output ...
Time taken to execute: ...
[blank line from pretty_printer — after]

Decorator Template

Use this as a starting point for any decorator:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        # --- code before ---
        result = func(*args, **kwargs)
        # --- code after ---
        return result
    return wrapper

Key Takeaways & Recap

Concept Summary
Decorator A function that wraps another function to add behaviour
@decorator syntax Shorthand for func = decorator(func)
Wrapper function The inner closure that runs before/after the original function
*args, **kwargs Required in wrapper so the decorator works for any function
Return the result Always return result — omitting this silently returns None
Multiple decorators Applied bottom-up (closest to the function first)
_ variable Anonymous placeholder when a loop variable isn't needed
time.time() Returns seconds since Jan 1, 1970 — useful for measuring elapsed time