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 function

3️⃣ 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 function

4️⃣ 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
# 5

5️⃣ Why functools.wraps is important

Without it:

print(hello.__name__)  # wrapper

Fix:

from functools import wraps
 
def my_decorator(func):
    @wraps(func)
    def wrapper(*args):
        return func(*args)
    return wrapper

Now: 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 prod

7️⃣ 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 denied

8️⃣ 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
# Hi

9️⃣ 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 caseExample
Loggingfunction call tracking
Authcheck user permission
Timingmeasure execution time
Retryretry failed operations
Cachingmemoization
Web frameworksFlask, 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