Course Progress63%
🍎 Python Advanced Python Topic 63 / 100
⏳ 8 min read

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."

— ShurAI

Functions as Arguments

In Python, functions are objects. You can pass them as arguments to other functions — this is the foundation decorators are built on:

python
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:

python
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):

python
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
Original function
say_hello()
just prints "Hello!"
Decorated function
say_hello()
Before → Hello! → After

Decorator for Functions with Arguments

Use *args, **kwargs in the wrapper so the decorator works with any function:

python
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

python
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})
output
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?