Decorators
Learn what decorators are, how they work, and how to write and use them in Python.
Decorators
A decorator is a function that wraps another function to add extra behaviour — without changing the original function's code.
That sounds abstract. Let's build up to it from scratch.
First — functions are objects
In Python, functions are just objects like anything else. You can store them in variables, pass them to other functions, and return them from functions.
def greet():
print("Hello!")
# store a function in a variable
say_hello = greet
say_hello() # Hello!say_hello and greet now point to the same function. We did not call it — we just stored it.
You can also pass a function as an argument to another function:
def greet():
print("Hello!")
def run(func):
func() # call whatever function was passed in
run(greet) # Hello!This is the foundation. Keep it in mind.
Second — functions inside functions
You can define a function inside another function:
def outer():
print("I am outer.")
def inner():
print("I am inner.")
inner()
outer()Output:
I am outer.
I am inner.inner only exists inside outer. You cannot call it from outside.
Third — returning a function
A function can return another function:
def outer():
def inner():
print("Hello from inner!")
return inner # return the function itself, not its result
func = outer()
func() # Hello from inner!outer() returns inner — not the result of calling inner, but the function object itself. Then we call it with func().
Now — what is a decorator?
A decorator is a function that:
- Takes a function as input
- Defines a new wrapper function around it
- Returns the wrapper
def my_decorator(func):
def wrapper():
print("Before the function runs.")
func()
print("After the function runs.")
return wrapperTo use it, pass your function through the decorator:
def greet():
print("Hello!")
greet = my_decorator(greet)
greet()Output:
Before the function runs.
Hello!
After the function runs.What happened? my_decorator took greet, wrapped it inside wrapper, and returned wrapper. Now greet points to wrapper. When you call greet(), you are actually calling wrapper() — which runs the extra code before and after the original greet().
The @ syntax — cleaner way to decorate
Writing greet = my_decorator(greet) every time is clunky. Python gives you a cleaner syntax using @:
def my_decorator(func):
def wrapper():
print("Before the function runs.")
func()
print("After the function runs.")
return wrapper
@my_decorator
def greet():
print("Hello!")
greet()Output:
Before the function runs.
Hello!
After the function runs.@my_decorator above the function is exactly the same as writing greet = my_decorator(greet). It is just cleaner.
Decorating functions with arguments
The simple wrapper above only works for functions that take no arguments. To make it work for any function, use *args and **kwargs in the wrapper:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before.")
result = func(*args, **kwargs)
print("After.")
return result
return wrapper
@my_decorator
def add(a, b):
return a + b
print(add(3, 4))Output:
Before.
After.
7*args and **kwargs collect whatever arguments were passed to add and forward them to the original function. Now your decorator works on any function regardless of its arguments.
Real example 1 — a timer decorator
Measure how long any function takes to run:
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds.")
return result
return wrapper
@timer
def calculate_sum(n):
return sum(range(n))
print(calculate_sum(1_000_000))Output:
calculate_sum took 0.0412 seconds.
499999500000You did not touch calculate_sum at all. The timer behaviour was added on top by the decorator.
Real example 2 — a login required decorator
A very common pattern in web development — check if a user is logged in before running a function:
def login_required(func):
def wrapper(user, *args, **kwargs):
if not user.get("is_logged_in"):
print("Access denied. Please log in first.")
return
return func(user, *args, **kwargs)
return wrapper
@login_required
def view_dashboard(user):
print(f"Welcome to your dashboard, {user['name']}!")
@login_required
def view_settings(user):
print(f"Opening settings for {user['name']}.")
# logged in user
user_ali = {"name": "Ali", "is_logged_in": True}
# not logged in
user_sara = {"name": "Sara", "is_logged_in": False}
view_dashboard(user_ali) # Welcome to your dashboard, Ali!
view_dashboard(user_sara) # Access denied. Please log in first.
view_settings(user_ali) # Opening settings for Ali.
view_settings(user_sara) # Access denied. Please log in first.One decorator — applied to two functions. Both get the login check without repeating the logic inside each function.
Real example 3 — a repeat decorator
Run a function multiple times:
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Ali")Output:
Hello, Ali!
Hello, Ali!
Hello, Ali!This is a decorator with arguments — notice the extra outer function repeat(times) that receives the argument and returns the actual decorator. The structure is one level deeper but the idea is the same.
When a decorator needs its own arguments like @repeat(3), you need three levels:
- Outer function takes the decorator's argument
- Middle function takes the original function
- Inner wrapper calls the original function
When a decorator needs no arguments like @timer, you only need two levels.
Stacking decorators
You can apply multiple decorators to one function. They are applied from bottom to top:
def bold(func):
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def italic(func):
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@bold
@italic
def greet():
return "Hello"
print(greet()) # <b><i>Hello</i></b>@italic is applied first (closest to the function), then @bold wraps around it.
functools.wraps — keeping the original function's identity
When you decorate a function, the wrapper replaces it — including its name and docstring. This can cause confusion when debugging:
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet():
"""Says hello."""
print("Hello!")
print(greet.__name__) # wrapper — not greet!Fix this with functools.wraps:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet():
"""Says hello."""
print("Hello!")
print(greet.__name__) # greet
print(greet.__doc__) # Says hello.Always use @functools.wraps(func) inside your decorators. It is one line and it keeps the original function's name and docstring intact. It is considered best practice.
Summary
| Concept | Example |
|---|---|
| Basic decorator | def decorator(func): |
| Apply a decorator | @decorator |
| Handle any arguments | def wrapper(*args, **kwargs): |
| Decorator with arguments | Three nested functions |
| Stack decorators | Multiple @ lines above a function |
| Preserve function identity | @functools.wraps(func) |