Advanced⏱ 25 min

Decorators

Decorators let you modify or extend a function's behaviour without touching its source code. They power Flask routes, Django views, caching, logging, and more.

Functions are First-Class Objects

In Python, functions are objects. They can be stored in variables, passed as arguments, and returned from other functions. Decorators rely entirely on this.

python
def greet(name):
    return f"Hello, {name}!"

say_hi = greet            # store function in variable
print(say_hi("Alice"))    # Hello, Alice!

def apply(func, value):   # function as argument
    return func(value)

print(apply(greet, "Bob")) # Hello, Bob!

def make_greeter(word):   # function returned from function
    def greeter(name):
        return f"{word}, {name}!"
    return greeter

hello = make_greeter("Hello")
hey   = make_greeter("Hey")
print(hello("Carol"))     # Hello, Carol!
print(hey("Dave"))        # Hey, Dave!
Output

Building a Decorator

A decorator is a function that takes a function and returns a new function wrapping the original.

python
import functools

def logger(func):
    @functools.wraps(func)  # keep original name/docstring
    def wrapper(*args, **kwargs):
        print(f"→ calling {func.__name__}{args}")
        result = func(*args, **kwargs)
        print(f"← returned {result!r}")
        return result
    return wrapper

# Manual application
def add(a, b): return a + b
logged_add = logger(add)
logged_add(3, 4)

print("---")

# @ syntax — exactly the same thing
@logger
def multiply(x, y):
    return x * y

multiply(5, 6)
Output
🔑
@ is sugar@logger above def multiply is exactly multiply = logger(multiply).

Practical Decorators

python
import time, functools

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

# Decorator with argument — needs an extra layer
def repeat(n):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@timer
def slow_sum(n):
    return sum(range(n))

@repeat(3)
def say(msg):
    print(msg)

slow_sum(1_000_000)
say("echo!")

# Simple cache (memoisation)
def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fib(n):
    if n <= 1: return n
    return fib(n-1) + fib(n-2)

print([fib(i) for i in range(10)])
Output
🎉

Lesson complete!

Next: Generators — memory-efficient iteration.

🏆

Certificate Unlocked!

You completed all lessons with 70%+. View your certificate →