Decorators
Wrap any function with extra behaviour using the @decorator syntax — without modifying the function itself.
"A decorator wraps a function and adds extra behaviour — without touching the function's own code. It's like putting a gift in a box: the gift hasn't changed, but now it comes with a ribbon."
— ShurAIFunctions as Arguments
In Python, functions are objects. You can pass them as arguments to other functions — this is the foundation decorators are built on:
def greet():
print("Hello!")
def run_twice(func): # takes a function as argument
func()
func()
run_twice(greet) # pass greet without calling it
# Hello!
# Hello!
Building a Decorator from Scratch
A decorator is a function that takes a function, wraps it in a new function with extra behaviour, and returns the wrapper:
def shout(func): # the decorator
def wrapper():
print(">>> Before the function")
func()
print("<<< After the function")
return wrapper # return the wrapper, not wrapper()
def say_hello():
print("Hello!")
# Manually decorate: wrap say_hello with shout
say_hello = shout(say_hello)
say_hello()
# >>> Before the function
# Hello!
# <<< After the function
The @ Syntax — Clean Decoration
@decorator_name placed above a function is just a shortcut for func = decorator(func):
def shout(func):
def wrapper():
print(">>> Before")
func()
print("<<< After")
return wrapper
@shout # same as: say_hello = shout(say_hello)
def say_hello():
print("Hello!")
say_hello() # automatically wrapped
Decorator for Functions with Arguments
Use *args, **kwargs in the wrapper so the decorator works with any function:
import time
def timer(func):
"""Measures how long a function takes to run."""
def wrapper(*args, **kwargs): # accept any arguments
start = time.time()
result = func(*args, **kwargs) # call the original
end = time.time()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
@timer
def add_numbers(a, b):
return a + b
print(add_numbers(3, 7))
# add_numbers took 0.0000s
# 10
Real Example — Login Required Decorator
def login_required(func):
"""Block access unless the user is logged in."""
def wrapper(user, *args, **kwargs):
if not user.get("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']}!")
view_dashboard({"name": "Riya", "logged_in": True})
view_dashboard({"name": "Guest", "logged_in": False})
Welcome to your dashboard, Riya!
Access denied. Please log in first.
"Decorators shine when many functions need the same extra behaviour — logging, timing, auth checks, caching. Write the logic once as a decorator and apply it anywhere with @."
— ShurAI🧠 Quiz — Q1
What is a decorator?
🧠 Quiz — Q2
What does @my_decorator above a function definition do?
🧠 Quiz — Q3
Why use *args, **kwargs in the wrapper function inside a decorator?
🧠 Quiz — Q4
You have a decorator @timer that logs how long a function runs. What is the main benefit?