🎓 Advanced · Topic 01
Decorators & Higher-Order Functions
Learn how to wrap, extend, and transform functions without modifying their source code — one of Python's most powerful meta-programming tools.
⏱ ~50 min
🔴 Advanced
🧠 Meta-programming
Functions as First-Class Objects
In Python, functions are objects — they can be assigned to variables, passed as arguments, and returned from other functions.
first_class.py
def shout(text):
return text.upper()
# Assign function to variable
yell = shout
print(yell("hello")) # HELLO
# Pass function as argument
def apply(func, value):
return func(value)
print(apply(shout, "world")) # WORLD
# Return function from function
def multiplier(n):
def inner(x):
return x * n
return inner
double = multiplier(2)
print(double(5)) # 10
Building a Decorator from Scratch
A decorator is a function that takes another function and returns an enhanced version of it. The @ syntax is just shorthand for reassignment.
decorator_basics.py
def timer(func):
import time
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
# Apply with @ syntax
@timer
def slow_sum(n):
return sum(range(n))
slow_sum(1_000_000)
# slow_sum took 0.0412s
# Equivalent without @ syntax:
# slow_sum = timer(slow_sum)
Preserving Metadata with functools.wraps
Without @wraps, the wrapped function loses its name and docstring. Always use it in production decorators.
wraps.py
from functools import wraps
def log(func):
@wraps(func) # preserves __name__, __doc__
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log
def greet(name):
"""Say hello."""
return f"Hello, {name}"
print(greet.__name__) # greet (not 'wrapper')
print(greet.__doc__) # Say hello.
Decorators with Arguments
To pass arguments to a decorator, add one more layer — a factory function that returns the actual decorator.
decorator_args.py
from functools import wraps
def repeat(times): # factory
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hi():
print("Hi!")
say_hi() # prints "Hi!" three times
💡 Real-world uses Decorators power @app.route in Flask, @login_required in Django, @cache in functools, and @pytest.mark in testing.
Stacking Multiple Decorators
Multiple decorators apply bottom-up: the one closest to the function is applied first.
stacking.py
@log # applied second (outer)
@timer # applied first (inner)
def process(data):
return [x * 2 for x in data]
# Equivalent to: process = log(timer(process))