1️⃣ What is a Decorator (mental model)
📌 Definition
A decorator is a function that:
- takes another function as input
- adds extra behavior
- returns a new function
- without modifying the original function code
Think:
function → decorator → enhanced function
2️⃣ First decorator (manual way)
def my_decorator(func):
def wrapper():
print("Before function")
func()
print("After function")
return wrapper
def hello():
print("Hello")
hello = my_decorator(hello)
hello()Output:
# Before function
# Hello
# After function3️⃣ Using @ syntax (same thing, cleaner)
def my_decorator(func):
def wrapper():
print("Before function")
func()
print("After function")
return wrapper
@my_decorator # same as hello = my_decorator(hello)
def hello():
print("Hello")
hello()Output:
# Before function
# Hello
# After function4️⃣ Decorator with arguments in function
Problem:
@my_decorator
def add(a, b):
print(a + b)Solution: use *args and **kwargs
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(2, 3))Output:
# Before
# After
# 55️⃣ Why functools.wraps is important
Without it:
print(hello.__name__) # wrapperFix:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args):
return func(*args)
return wrapperNow: it preserves the metadata
# hello.__name__ == "hello"6️⃣ Real-world decorator: logging
def log(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}") # Calling deploy
return func(*args, **kwargs)
return wrapper
@log
def deploy(env):
print(f"Deploying to {env}")
deploy("prod")Output:
# Calling deploy
# Deploying to prod7️⃣ Auth + Logging decorator (practical example)
from functools import wraps
import datetime
def admin_only(func):
@wraps(func)
def wrapper(user_role):
timestamp = datetime.datetime.now().isoformat()
# logging attempt
print(f"[{timestamp}] Attempt by role='{user_role}' to call {func.__name__}")
if user_role != "admin":
print(f"[{timestamp}] ❌ Access denied")
return "Admins only"
else:
print(f"[{timestamp}] ✅ Access granted")
return func(user_role)
return wrapper
@admin_only
def delete_user(user_role):
print("User deleted successfully")
return "OK"▶️ Execution
❌ Non-admin attempt
delete_user("guest")Output:
[2026-01-10T17:10:21] Attempt by role='guest' to call delete_user
[2026-01-10T17:10:21] ❌ Access denied8️⃣ Decorator with parameters (advanced)
def repeat(n):
def decorator(func):
def wrapper(*args):
for _ in range(n):
result = func(*args)
return result
return wrapper
return decorator
@repeat(3)
def say_hi():
print("Hi")
say_hi()Output:
# Hi
# Hi
# Hi9️⃣ Multiple decorators (stacking)
def star(func):
def wrapper():
print("***")
func()
print("***")
return wrapper
def dash(func):
def wrapper():
print("---")
func()
print("---")
return wrapper
@star
@dash
def hello():
print("Hello")
hello()Execution order:
# ***
# ---
# Hello
# ---
# ***Rule:
- decorators apply bottom-up
- execution happens top-down
🔟 Where decorators are used in real world
| Use case | Example |
|---|---|
| Logging | function call tracking |
| Auth | check user permission |
| Timing | measure execution time |
| Retry | retry failed operations |
| Caching | memoization |
| Web frameworks | Flask, FastAPI routes |
1️⃣1️⃣ Simple timing decorator (real use)
import time
def timer(func):
def wrapper(*args):
start = time.time()
result = func(*args)
end = time.time()
print(f"Took {end - start:.2f}s")
return result
return wrapper
@timer
def work():
time.sleep(1)
work()Output:
# Took 1.00s🧠 Final memory cheat
# decorator = function that returns wrapper
# wrapper = function that runs before and after original
# @decorator is syntax sugar
# *args, **kwargs make it universal
# wraps keeps metadata