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 |