Python

P
PythonHub
Progress 0%

Python

  • Home
  • History of Python
  • Applications of Python
  • Introduction To Python
    • What is Python
    • Character Set
    • Tokens in Python
    • Python Execution Mode
    • Variable And Identifiers
    • Data Types in Python
    • Operators And Expressions
    • Constants in Python
    • Assignment Statement
    • Input / Output in Python
    • Simple Python Scripts
    • Namespace in Python
    • 📝 Assignments
  • Operators in Python
    • Arithmetic Operators
    • Assignment Operators
    • Shorthand Assignment Operators
    • Relational Operators
    • Logical Operators
    • Bitwise Operators
    • Special Operators
    • 📝 Assignments
  • Input Output in Python
    • Accept Input
    • Output Formatting
    • 📝 Assignments
  • Conditional Statement
    • Decision Making
    • if Statement
    • IF-ELSE STATEMENT
    • IF-ELSE LADDER
    • NESTED IF-ELSE
    • Short Hand IF-ELSE
    • 📝 Assignments
  • Loops
    • Introduction to Loops
    • While Loop
    • Nested While Loop
    • 📝 While Loop Assignments
    • For Loop
    • For Loop Examples
    • Nested For Loop
    • Nested For Loop Examples
    • Infinite While Loops
    • Infinite For Loops
    • Break, Continue and Else in Loops
    • Difference Between For and While Loop
    • For Each Loop
    • 📝 For Each Assignments
    • 📝 All Loops Assignments
  • List
    • List in Python
    • Access List Elements
    • List Functions
    • Iterate (Loop) List
    • List Comprehension
    • 📝 Assignments
  • Tuple
    • Tuple in Python
    • Access Tuple Elements
    • Tuple Functions
    • Iterate (Loop) Tuple
    • Unpack Tuple
    • Tuple Comprehension
    • 📝 Assignments
  • Set
    • Set in Python
    • Access Set Elements
    • Set Methods
    • Iterate (Loop) Set
    • Pack/Unpack Set
    • Set Comprehension
    • 📝 Assignments
  • Dictionary
    • Dictionary
    • Access Dictionary Items
    • Dictionary Methods
    • Iterate (Loop) Dictionary
    • Formatting Dictionaries
    • Nested Dictionaries
    • Dictionary Comprehension
    • 📝 Assignments
  • Diff List Tuple Set Dictionary
    • List vs Tuple
    • List vs Set
    • List vs Dictionary
    • Tuple vs Set
    • Tuple vs Dictionary
    • Dictionary vs Set
    • 📝 Assignments
  • Exception
    • Error vs Exception
    • Types of Exception
    • Exception Handling
    • User Defined Exception
    • Logging Exception
    • 📝 Assignments
  • Functions
    • Introduction to Functions
    • Modular Programming
    • Types of Functions
    • Inbuilt Functions
    • Need For User-Defined Function
    • Elements of User Defined Function
    • Function Arguments
    • Nesting of Functions
    • Recursion
    • Global Local and Non Local
    • Python Lambda Functions
    • 📝 Assignments
  • Python Module
    • Introduction to Module
    • Inbuilt Modules in Python
    • User Defined Module
    • 📝 Assignments
  • File Handling
    • Introduction to Files
    • Create File
    • Read Files
    • Write to File
    • Rename File
    • Copy File
    • Move File
    • List Files in Directory
    • Binary Files
    • Zipping and Unzipping Files
    • 📝 Assignments
  • Strings
    • Basics of Strings
    • String Special Operators
    • String Formatting Operators
    • String Methods
    • 📝 Assignments
  • Regular Expressions
  • Python OOPS
    • Basics of Object Oriented
    • What are Classes and Objects?
    • Creating Class and Object
    • OOP vs Procedural Programming
    • Difference Between Classes and Objects
    • Constructors
    • Destructor
    • Built Class Methods and Attributes
    • Class and Instance Variables
    • Inheritance in Python
    • Single Inheritance
    • Multiple Inheritance
    • Multilevel Inheritance
    • Hierarchical Inheritance
    • Hybrid Inheritance
    • Abstraction
    • Method Overriding
    • Abstract Method
    • Interfaces in Python
    • Abstract Class vs Interface
    • Public, Private and Protected
    • Overloading vs Overriding
    • Inheritance vs Composition
    • Encapsulation
    • Polymorphism
    • Inner Classes
    • 📝 Assignments
  • Advanced Python
    • Iterator in Python
    • Generator in Python
    • Decorator in Python
    • Type Hints in Python
    • Async/Await Programming
    • Dataclasses in Python
    • Context Managers in Python
    • Property Decorator in Python
    • __slots__ in Python
    • Enums in Python
    • Walrus Operator in Python
    • Match-Case in Python
    • 📝 Assignments
  • Python Standard Library
    • Collections Module
    • Itertools Module
    • Functools Module
    • Datetime Module
    • JSON Module
    • OS Module
    • Sys Module
    • Random Module
    • Math Module
    • 📝 Assignments
  • Python Testing
    • Unit Testing in Python
    • Pytest Framework
    • Mocking in Python
    • 📝 Assignments
  • Python Best Practices
    • PEP 8 Style Guide
    • Docstrings in Python
    • Logging in Python
    • Code Optimization Tips
    • Debugging Techniques
    • 📝 Assignments
  • MySQL Database in Python
    • Introduction to MySQL with Python
    • DBMS vs File System
    • Connecting to MySQL Database
    • Create Database in MySQL
    • Create Table in MySQL
    • Insert Data in MySQL
    • Insert Multiple Rows
    • Select Data from MySQL
    • WHERE Clause in MySQL
    • Update Data in MySQL
    • Delete Data from MySQL
    • Parameterized Queries
    • Transaction Management
    • Error Handling
    • Connection Pooling
    • MySQL Drivers Guide
    • Joins in MySQL
    • Aggregation Functions
    • Backup MySQL Database
    • Best Practices
    • 📝 Assignments
  • MySQL Database Operations
    • SELECT Statement
    • MySQL Operators
    • DDL Statements
    • DML Statements
    • Subqueries
    • JOIN Operations
    • Aggregation
    • Case Study
    • 📝 Assignments
  • Graphics in Python
  • Threads in Python
    • Introduction to Threads
    • Process vs Threads
    • Concurrent Programming & GIL
    • Uses of Threads
    • Creating Threads
    • Single Tasking
    • Multi Tasking
    • Thread Synchronization
    • 📝 Assignments
  • Interview Questions & Answers
  • Python Case Studies
  • Multiple Choice Questions
  • 📝 Practice Problems
Get in Touch
  • tech2dsm@gmail.com

© Sankalan Data Tech

Python Language Interactive Tutorial

Python: Decorators

Python Decorators - Complete Guide

Learn how to add extra features to your functions using decorators.

Created by Sankalan Data Tech Team Verified
Data Engineers, Analysts, Scientists & Trainers
Created by experienced Python developers, data engineers, and data scientists to make programming easy through practical examples, real-world experience, and clear explanations.
On this page:
  • What are Decorators?
  • Why Use Decorators?
  • How Decorators Work
  • Creating Your Own Decorators
  • Decorators with Arguments
  • Chaining Decorators
  • Real-World Example
  • Best Practices
  • Try It Yourself
  • Quick Quiz
  • Frequently Asked Questions
Share this tutorial:
Twitter LinkedIn Facebook WhatsApp Reddit Telegram Email Copy Link
What You'll Learn Here
  • What are decorators — functions that add extra features to other functions
  • Why use decorators — keep code clean, reusable, and organized
  • How they work — the @ syntax and wrapper functions
  • Create your own — build custom decorators
  • Real-world examples — logging, timing, authentication
  • Best practices — using functools.wraps and more

What are Decorators?

A decorator is a special function that adds extra features to another function. It's like a wrapper that goes around your function and adds something before or after it runs.

Think of a decorator like a gift wrapper. You have a gift (your function). You wrap it in nice paper (the decorator). The gift is still the same, but now it looks better and maybe has a card attached.

Decorators use the @ symbol in Python. You put @decorator_name right above your function definition.

Key concept: A decorator is a function that takes another function as an argument, adds some behavior, and returns the modified function.

Why Use Decorators?

1

The Benefits of Decorators

Decorators make your code cleaner and more organized. Here's why you should use them:

# Why Use Decorators?

print("=" * 50)
print("WHY USE DECORATORS?")
print("=" * 50)

# ============================================================
# WITHOUT DECORATORS — Repetitive Code
# ============================================================

print("\n WITHOUT DECORATORS:")

def greet(name):
    print("=" * 20)
    print(f"Hello {name}!")
    print("=" * 20)

def farewell(name):
    print("=" * 20)
    print(f"Goodbye {name}!")
    print("=" * 20)

greet("Alice")
farewell("Bob")

print("\n   Problem: The border code is repeated in EVERY function")
print("   Problem: If we change the border, we change it everywhere")


# ============================================================
# WITH DECORATORS — Clean and Reusable
# ============================================================

print("\n WITH DECORATORS:")

def add_border(func):
    """Decorator that adds a border around the output"""
    def wrapper(name):
        print("=" * 20)
        func(name)
        print("=" * 20)
    return wrapper

@add_border
def greet_with_border(name):
    print(f"Hello {name}!")

@add_border
def farewell_with_border(name):
    print(f"Goodbye {name}!")

greet_with_border("Alice")
farewell_with_border("Bob")

print("\n Benefits:")
print("  1. No repetition — border code is in one place")
print("  2. Easy to change — update the decorator once")
print("  3. Clean code — functions focus on their main job")
print("  4. Reusable — use the same decorator anywhere")


# ============================================================
# COMMON USES OF DECORATORS
# ============================================================

print("\n" + "-" * 30)
print("COMMON USES")
print("-" * 30)
print("""
┌─────────────────────┬────────────────────────────────────────────┐
│ USE CASE            │ WHY IT HELPS                              │
├─────────────────────┼────────────────────────────────────────────┤
│ Logging             │ Track when functions run                 │
│ Timing              │ Measure how long functions take          │
│ Authentication      │ Check if user is logged in               │
│ Caching             │ Store results to avoid recalculating     │
│ Rate limiting       │ Limit how often a function can run       │
│ Permission checks   │ Check if user has permission             │
└─────────────────────┴────────────────────────────────────────────┘
""")

Benefits of decorators:

  • No repetition — write code once, use it many times
  • Easy to maintain — change one place, not many
  • Clean code — functions focus on their main job
  • Reusable — use the same decorator on many functions
  • Separates concerns — keeps different logic separate

Quick Check: What problem do decorators solve? (Answer: They help avoid repetitive code by adding common behavior to multiple functions)

How Decorators Work

2

The Magic Behind Decorators

To understand decorators, you need to know two things about Python:

  • Functions are objects — you can pass them around like any other value
  • Functions can be nested — you can define a function inside another function

A decorator is just a function that takes a function, wraps it, and returns the wrapped version.

# How Decorators Work

print("=" * 50)
print("HOW DECORATORS WORK")
print("=" * 50)

# ============================================================
# STEP 1: Functions are Objects
# ============================================================

print("\n1. FUNCTIONS ARE OBJECTS")

def say_hello(name):
    return f"Hello {name}!"

# You can assign a function to a variable
greet = say_hello
print(f"   say_hello('Alice'): {say_hello('Alice')}")
print(f"   greet('Alice'): {greet('Alice')}")

# You can pass functions as arguments
def call_function(func, value):
    return func(value)

print(f"   call_function(say_hello, 'Bob'): {call_function(say_hello, 'Bob')}")


# ============================================================
# STEP 2: Nested Functions
# ============================================================

print("\n2. NESTED FUNCTIONS")

def outer_function(value):
    print(f"   Outer function called with: {value}")
    
    def inner_function():
        return f"   Inner function using: {value}"
    
    return inner_function

inner = outer_function("test")
print(inner())


# ============================================================
# STEP 3: Creating a Simple Decorator
# ============================================================

print("\n3. CREATING A SIMPLE DECORATOR")

def simple_decorator(func):
    """A simple decorator that adds a message"""
    def wrapper():
        print("    Before function")
        func()
        print("    After function")
    return wrapper

# Manual way (without @)
def say_hi():
    print("   Hi!")

decorated = simple_decorator(say_hi)
decorated()

# With @ (the Python way)
@simple_decorator
def say_hello_decorated():
    print("   Hello!")

say_hello_decorated()


# ============================================================
# STEP 4: What the @ Does
# ============================================================

print("\n4. WHAT THE @ DOES")

# This code:
@simple_decorator
def my_function():
    print("   Function body")

# Is exactly the same as:
def my_function():
    print("   Function body")
my_function = simple_decorator(my_function)

print("    @ is just a shortcut!")
print("   The decorator replaces the original function with the wrapper")


# ============================================================
# THE COMPLETE PICTURE
# ============================================================

print("\n" + "-" * 30)
print("SUMMARY — HOW DECORATORS WORK")
print("-" * 30)
print("""
1. Python sees the @ symbol above your function
2. It takes your function and passes it to the decorator
3. The decorator returns a new function (the wrapper)
4. Your function name now points to the wrapper
5. When you call your function, the wrapper runs first
""")

How decorators work key points:

  • Functions are objects — they can be passed as arguments
  • Nested functions — functions can be defined inside other functions
  • Wrapper pattern — the decorator creates a wrapper function
  • @ syntax — shorthand for func = decorator(func)
  • Replaces the original — the original function is replaced by the wrapper

Quick Check: What does the @ symbol do in Python? (Answer: It applies a decorator to a function, replacing the function with the decorated version)

Creating Your Own Decorators

3

Building Custom Decorators

Now let's create some useful decorators you can use in your projects.

# Creating Your Own Decorators

print("=" * 50)
print("CREATING YOUR OWN DECORATORS")
print("=" * 50)

# ============================================================
# DECORATOR 1: Timing Function Execution
# ============================================================

print("\n1. TIMING DECORATOR")

import time

def timer(func):
    """Decorator that times how long a function takes"""
    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 slow_function():
    time.sleep(0.5)
    return "Done"

print(f"   Result: {slow_function()}")


# ============================================================
# DECORATOR 2: Logging
# ============================================================

print("\n2. LOGGING DECORATOR")

def logger(func):
    """Decorator that logs when a function is called"""
    def wrapper(*args, **kwargs):
        print(f"    Calling {func.__name__}")
        print(f"    Arguments: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"    Result: {result}")
        return result
    return wrapper

@logger
def add_numbers(a, b):
    return a + b

@logger
def greet_person(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(add_numbers(5, 3))
print(greet_person("Alice", greeting="Hi"))


# ============================================================
# DECORATOR 3: Retry on Error
# ============================================================

print("\n3. RETRY DECORATOR")

def retry(max_attempts=3, delay=1):
    """Decorator that retries a function if it fails"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    print(f"    Attempt {attempts} failed: {e}")
                    if attempts < max_attempts:
                        print(f"    Retrying in {delay} seconds...")
                        time.sleep(delay)
            raise Exception(f"Function failed after {max_attempts} attempts")
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def flaky_function():
    import random
    if random.random() < 0.6:
        raise ValueError("Random failure!")
    return "Success!"

print("   Trying flaky function:")
try:
    result = flaky_function()
    print(f"   Result: {result}")
except Exception as e:
    print(f"    {e}")


# ============================================================
# DECORATOR 4: Authentication (Simple)
# ============================================================

print("\n4. AUTHENTICATION DECORATOR")

def require_login(func):
    """Decorator that checks if user is logged in"""
    # In real code, this would check a session or token
    def wrapper(*args, **kwargs):
        is_logged_in = True  # Simulate login status
        if is_logged_in:
            return func(*args, **kwargs)
        else:
            return " Authentication required!"
    return wrapper

@require_login
def view_profile(user_id):
    return f" Profile for user {user_id}"

@require_login
def edit_settings():
    return " Settings updated"

print(f"   {view_profile(123)}")
print(f"   {edit_settings()}")


# ============================================================
# DECORATOR 5: Caching Results
# ============================================================

print("\n5. CACHING DECORATOR")

def cache(func):
    """Decorator that caches function results"""
    cache_dict = {}
    
    def wrapper(*args):
        if args in cache_dict:
            print(f"    Using cached value for {args}")
            return cache_dict[args]
        result = func(*args)
        cache_dict[args] = result
        print(f"    Computing new value for {args}")
        return result
    return wrapper

@cache
def expensive_function(x):
    time.sleep(0.3)  # Simulate expensive computation
    return x * x

print(expensive_function(5))  # Computes
print(expensive_function(5))  # Uses cache
print(expensive_function(10)) # Computes
print(expensive_function(10)) # Uses cache

Custom decorators key points:

  • timer — measures how long a function takes
  • logger — logs function calls and arguments
  • retry — retries a function if it fails
  • require_login — checks authentication
  • cache — stores results to avoid recomputation

Quick Check: What does the timer decorator do? (Answer: It measures and prints how long a function takes to run)

Decorators with Arguments

4

Passing Arguments to Decorators

Sometimes you want to customize your decorator. For example, you might want to specify how many times to retry, or what logging level to use.

To do this, you need a decorator factory — a function that returns a decorator.

# Decorators with Arguments

print("=" * 50)
print("DECORATORS WITH ARGUMENTS")
print("=" * 50)

# ============================================================
# DECORATOR WITH ARGUMENTS — Retry with custom attempts
# ============================================================

print("\n1. RETRY WITH CUSTOM ATTEMPTS")

def retry_with_attempts(attempts):
    """Decorator factory that retries a function"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            for attempt in range(attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"   Attempt {attempt + 1} failed: {e}")
                    if attempt == attempts - 1:
                        raise
            return None
        return wrapper
    return decorator

@retry_with_attempts(2)
def fragile_function():
    raise ValueError("Something went wrong!")

try:
    fragile_function()
except ValueError as e:
    print(f"    All attempts failed: {e}")


# ============================================================
# DECORATOR WITH ARGUMENTS — Log with custom message
# ============================================================

print("\n2. LOG WITH CUSTOM MESSAGE")

def log_with_message(message):
    """Decorator factory that logs with a custom message"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"    {message}")
            print(f"    Calling {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log_with_message("Starting database operation...")
def save_to_db(data):
    return f"Saved: {data}"

@log_with_message("Starting API call...")
def fetch_from_api():
    return "API data"

print(save_to_db("user_data"))
print(fetch_from_api())


# ============================================================
# DECORATOR WITH ARGUMENTS — Rate Limiting
# ============================================================

print("\n3. RATE LIMITING")

def rate_limit(limit_per_second=1):
    """Decorator that limits how often a function can run"""
    last_called = [0]  # Use a list to store mutable state
    
    def decorator(func):
        def wrapper(*args, **kwargs):
            current_time = time.time()
            time_since_last = current_time - last_called[0]
            
            if time_since_last < 1.0 / limit_per_second:
                print(f"    Rate limit hit. Wait {1.0/limit_per_second - time_since_last:.2f}s")
                time.sleep(1.0/limit_per_second - time_since_last)
            
            last_called[0] = time.time()
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limit(limit_per_second=2)
def send_message(msg):
    return f" Sending: {msg}"

print(send_message("Hello"))
print(send_message("World"))
print(send_message("Test"))


# ============================================================
# UNDERSTANDING DECORATOR FACTORIES
# ============================================================

print("\n4. UNDERSTANDING DECORATOR FACTORIES")

print("""
┌─────────────────────────────────────────────────────────────────────┐
│ DECORATOR FACTORY — A Function That Returns a Decorator           │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  def decorator_factory(arg1, arg2):                                │
│      def decorator(func):                                          │
│          def wrapper(*args, **kwargs):                             │
│              # Do something with arg1, arg2                       │
│              result = func(*args, **kwargs)                       │
│              return result                                         │
│          return wrapper                                            │
│      return decorator                                              │
│                                                                     │
│  @decorator_factory("hello", 5)                                   │
│  def my_function():                                               │
│      pass                                                          │
│                                                                     │
│  This allows you to customize decorators with arguments.          │
└─────────────────────────────────────────────────────────────────────┘
""")

Decorators with arguments key points:

  • Decorator factory — a function that returns a decorator
  • Three levels — factory → decorator → wrapper
  • Customization — pass arguments to control the decorator's behavior
  • Common uses — retry attempts, log messages, rate limits

Quick Check: What is a decorator factory? (Answer: A function that takes arguments and returns a decorator)

Chaining Decorators

5

Using Multiple Decorators

You can apply multiple decorators to one function. They stack from bottom to top (closest to the function runs first).

# Chaining Decorators

print("=" * 50)
print("CHAINING DECORATORS")
print("=" * 50)

# ============================================================
# DEFINE SOME DECORATORS
# ============================================================

def bold(func):
    """Add bold formatting"""
    def wrapper(*args, **kwargs):
        return f"**{func(*args, **kwargs)}**"
    return wrapper

def italic(func):
    """Add italic formatting"""
    def wrapper(*args, **kwargs):
        return f"*{func(*args, **kwargs)}*"
    return wrapper

def underline(func):
    """Add underline formatting"""
    def wrapper(*args, **kwargs):
        return f"__{func(*args, **kwargs)}__"
    return wrapper

def repeat(n):
    """Repeat the output n times"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            return " ".join([result] * n)
        return wrapper
    return decorator


# ============================================================
# CHAINING DECORATORS
# ============================================================

print("\n1. CHAINING DECORATORS")

@bold
@italic
def greet(name):
    return f"Hello {name}!"

print(f"   {greet('Alice')}")

@underline
@bold
@italic
def farewell(name):
    return f"Goodbye {name}!"

print(f"   {farewell('Bob')}")


# ============================================================
# ORDER MATTERS
# ============================================================

print("\n2. ORDER MATTERS")

@bold
@italic
def get_message1(name):
    return f"Hi {name}!"

@italic
@bold
def get_message2(name):
    return f"Hi {name}!"

print(f"   bold(italic): {get_message1('Alice')}")
print(f"   italic(bold): {get_message2('Alice')}")


# ============================================================
# DECORATORS WITH ARGUMENTS CHAINED
# ============================================================

print("\n3. DECORATORS WITH ARGUMENTS CHAINED")

@repeat(3)
@bold
@italic
def announcement(msg):
    return f"ANNOUNCEMENT: {msg}"

print(f"   {announcement('Welcome to Python!')}")


# ============================================================
# HOW CHAINING WORKS
# ============================================================

print("\n4. HOW CHAINING WORKS")

print("""
┌─────────────────────────────────────────────────────────────────────┐
│ HOW CHAINING WORKS — Bottom to Top                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  @decorator1                                                       │
│  @decorator2                                                       │
│  @decorator3                                                       │
│  def my_function():                                               │
│      pass                                                          │
│                                                                     │
│  Is equivalent to:                                                 │
│                                                                     │
│  my_function = decorator1(decorator2(decorator3(my_function)))    │
│                                                                     │
│  decorator3 runs first (closest to the function)                  │
│  decorator2 runs second                                            │
│  decorator1 runs last (farthest from the function)                │
└─────────────────────────────────────────────────────────────────────┘
""")

Chaining decorators key points:

  • Multiple decorators — use many decorators on one function
  • Bottom to top — the closest decorator to the function runs first
  • Order matters — different orders give different results
  • Pattern — @outer @middle @inner runs inner first

Quick Check: Which decorator runs first when chaining? (Answer: The one closest to the function definition — bottom one)

Real-World Example

6

Building a Web API Handler

# Real-World Example: Web API Handler

import time
import json
from datetime import datetime

print("=" * 60)
print("WEB API HANDLER — DECORATORS IN ACTION")
print("=" * 60)

# ============================================================
# DECORATORS FOR WEB API
# ============================================================

def log_request(func):
    """Log every API request"""
    def wrapper(*args, **kwargs):
        print(f"    [{datetime.now().strftime('%H:%M:%S')}] {func.__name__} called")
        print(f"    Args: {args}")
        result = func(*args, **kwargs)
        print(f"    Response: {result[:50]}...")
        return result
    return wrapper

def authenticate(func):
    """Check if user is authenticated"""
    def wrapper(user, *args, **kwargs):
        if not user.get("authenticated", False):
            return {"error": "Authentication required", "status": 401}
        return func(user, *args, **kwargs)
    return wrapper

def rate_limit(limit_per_minute=60):
    """Rate limit API calls"""
    calls = []
    
    def decorator(func):
        def wrapper(*args, **kwargs):
            now = time.time()
            # Remove calls older than 1 minute
            calls[:] = [t for t in calls if now - t < 60]
            
            if len(calls) >= limit_per_minute:
                return {"error": "Rate limit exceeded", "status": 429}
            
            calls.append(now)
            return func(*args, **kwargs)
        return wrapper
    return decorator

def cache_result(func):
    """Cache API results for 5 seconds"""
    cache = {}
    
    def wrapper(*args, **kwargs):
        key = str(args) + str(kwargs)
        if key in cache:
            timestamp, result = cache[key]
            if time.time() - timestamp < 5:
                print(f"    Cache hit for {func.__name__}")
                return result
        
        result = func(*args, **kwargs)
        cache[key] = (time.time(), result)
        print(f"    Cache miss for {func.__name__}")
        return result
    return wrapper


# ============================================================
# API HANDLERS WITH DECORATORS
# ============================================================

print("\n1. API HANDLERS")

@log_request
@authenticate
@rate_limit(limit_per_minute=10)
def get_user_profile(user, user_id):
    """Get a user's profile"""
    return {"name": "Alice", "email": "alice@example.com", "id": user_id}

@log_request
@authenticate
@cache_result
def get_expensive_data(user, query):
    """Get expensive data with caching"""
    time.sleep(0.5)  # Simulate expensive operation
    return {"data": [1, 2, 3, 4, 5], "query": query}

@log_request
def public_endpoint():
    """Public endpoint — no authentication needed"""
    return {"message": "Welcome to the API!"}


# ============================================================
# TESTING THE API
# ============================================================

print("\n2. TESTING API ENDPOINTS")

# Test user
user1 = {"username": "alice", "authenticated": True}
user2 = {"username": "bob", "authenticated": False}

print("\n   --- Get User Profile (authenticated) ---")
result = get_user_profile(user1, 123)
print(f"   Result: {result}")

print("\n   --- Get User Profile (not authenticated) ---")
result = get_user_profile(user2, 123)
print(f"   Result: {result}")

print("\n   --- Get Expensive Data (first call — cache miss) ---")
result = get_expensive_data(user1, "python")
print(f"   Result: {result}")

print("\n   --- Get Expensive Data (second call — cache hit) ---")
result = get_expensive_data(user1, "python")
print(f"   Result: {result}")

print("\n   --- Public Endpoint ---")
result = public_endpoint()
print(f"   Result: {result}")

print("\n   --- Rate Limit Test ---")
for i in range(12):
    result = get_user_profile(user1, i)
    if isinstance(result, dict) and result.get("error") == "Rate limit exceeded":
        print(f"    Rate limit hit after {i} calls")
        break


# ============================================================
# DECORATOR PATTERNS SUMMARY
# ============================================================

print("\n" + "=" * 60)
print("DECORATOR PATTERNS SUMMARY")
print("=" * 60)
print("""
┌─────────────────────┬──────────────────────────────────────────────────┐
│ DECORATOR           │ PURPOSE                                         │
├─────────────────────┼──────────────────────────────────────────────────┤
│ log_request         │ Log every API call for debugging               │
│ authenticate        │ Check user authentication before accessing      │
│ rate_limit          │ Prevent API abuse by limiting requests         │
│ cache_result        │ Store results to speed up repeated queries      │
└─────────────────────┴──────────────────────────────────────────────────┘

 Real-world decorators you might use:
• @login_required — from Flask/Django
• @app.route — from Flask for URL routing
• @cache — from functools
• @property — Python's built-in property decorator
""")

Real-world example key points:

  • log_request — logs every API request
  • authenticate — checks if user is logged in
  • rate_limit — prevents API abuse
  • cache_result — speeds up repeated requests
  • Chained decorators — multiple decorators work together

Quick Check: What's the purpose of the cache_result decorator? (Answer: It stores results to avoid running expensive operations multiple times)

Best Practices

7

Using Decorators Effectively

# Best Practices for Decorators

print("=" * 60)
print("BEST PRACTICES FOR DECORATORS")
print("=" * 60)

# ============================================================
# 1. USE functools.wraps
# ============================================================

print("\n1. USE functools.wraps")

#  BAD: Without wraps — loses function metadata
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def my_function():
    """This is my function"""
    pass

print(f"    Without wraps:")
print(f"      Name: {my_function.__name__}")      # Shows 'wrapper', not 'my_function'
print(f"      Docstring: {my_function.__doc__}")  # Shows None


#  GOOD: With wraps — preserves metadata
from functools import wraps

def good_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@good_decorator
def my_good_function():
    """This is my good function"""
    pass

print(f"\n    With wraps:")
print(f"      Name: {my_good_function.__name__}")
print(f"      Docstring: {my_good_function.__doc__}")


# ============================================================
# 2. USE *args AND **kwargs
# ============================================================

print("\n2. USE *args AND **kwargs")

def flexible_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"   Calling with args: {args}, kwargs: {kwargs}")
        return func(*args, **kwargs)
    return wrapper

@flexible_decorator
def add(a, b):
    return a + b

@flexible_decorator
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(f"   add(5, 3): {add(5, 3)}")
print(f"   greet('Alice', greeting='Hi'): {greet('Alice', greeting='Hi')}")


# ============================================================
# 3. KEEP DECORATORS SIMPLE
# ============================================================

print("\n3. KEEP DECORATORS SIMPLE")

#  GOOD: Simple, focused decorator
def timer_simple(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"   {func.__name__} took {time.time() - start:.3f}s")
        return result
    return wrapper

#  BAD: Decorator doing too much
def over_complicated(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Timing, logging, caching, retry, authentication...
        # Too much! Split into multiple decorators
        pass
    return wrapper


# ============================================================
# 4. USE DECORATOR FACTORIES FOR CONFIGURATION
# ============================================================

print("\n4. USE DECORATOR FACTORIES FOR CONFIGURATION")

def repeat_n_times(n):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            results = []
            for _ in range(n):
                results.append(func(*args, **kwargs))
            return results
        return wrapper
    return decorator

@repeat_n_times(3)
def say_hi(name):
    return f"Hi {name}!"

print(f"   {say_hi('Alice')}")


# ============================================================
# 5. DOCUMENT YOUR DECORATORS
# ============================================================

print("\n5. DOCUMENT YOUR DECORATORS")

def documented_decorator(func):
    """
    This decorator does XYZ.
    Usage: @documented_decorator
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        """Wrapper function that does ABC"""
        return func(*args, **kwargs)
    return wrapper


# ============================================================
# 6. SUMMARY
# ============================================================

print("\n" + "=" * 60)
print("BEST PRACTICES SUMMARY")
print("=" * 60)
print("""
┌─────────────────────────────┬─────────────────────────────────────────────┐
│ PRACTICE                    │ WHY IT MATTERS                             │
├─────────────────────────────┼─────────────────────────────────────────────┤
│ Use functools.wraps         │ Preserves function name, docstring, etc   │
│                             │                                             │
│ Use *args and **kwargs      │ Works with any function signature          │
│                             │                                             │
│ Keep decorators simple      │ Easy to understand and maintain           │
│                             │                                             │
│ Use decorator factories     │ Customize behavior with arguments         │
│                             │                                             │
│ Document your decorators    │ Users know how to use them                │
└─────────────────────────────┴─────────────────────────────────────────────┘

 REMEMBER:
   • Always use @wraps to preserve function metadata
   • Use *args and **kwargs for flexibility
   • One decorator = one job
   • Chain decorators for multiple behaviors
""")

Best practices summary:

  • Use functools.wraps — preserves function metadata
  • Use *args and **kwargs — makes decorators flexible
  • Keep decorators simple — one decorator = one job
  • Use decorator factories — for customization with arguments
  • Document decorators — tell users how to use them

Quick Check: Why should you use @wraps in decorators? (Answer: It preserves the original function's name, docstring, and other metadata)

Try It Yourself

Experiment with decorators in the editor below.

Loading Pyodide... 0%
Python Code Editor
==================================================
DECORATORS - PRACTICE
==================================================

1. BASIC DECORATOR
🔍 Calling multiply
🔍 Args: (4, 5), {}
🔍 Result: 20
20
🔍 Calling greet
🔍 Args: ('Alice',), {'greeting': 'Hi'}
🔍 Result: Hi, Alice!
Hi, Alice!

2. DECORATOR WITH ARGUMENTS
say_hello('Bob') repeated 3 times:
['Hello Bob!', 'Hello Bob!', 'Hello Bob!']

3. CHAINING DECORATORS
shout_message('hello'): HELLO!!!

4. TIMING DECORATOR
count_to_n took 0.0234s
Result: 499999500000

You've Got It!

You now understand decorators in Python. You know how to create custom decorators, use decorator factories, and chain decorators together.

Quick Quiz

Test what you've learned:

1. What is a decorator in Python?
2. What does the @ symbol do in Python?
3. Why should you use @wraps in a decorator?
4. What is a decorator factory?
5. In chaining decorators, which one runs first?

Frequently Asked Questions

What is a decorator in Python? ▼

A decorator is a function that takes another function as an argument, adds some extra behavior, and returns the modified function. It's used to add features like logging, timing, or authentication without changing the original function's code.

What does @ do in Python? ▼

The @ symbol is syntactic sugar for applying a decorator. @my_decorator above a function is the same as writing my_function = my_decorator(my_function). It makes decorators easier to read and use.

Why use @wraps in decorators? ▼

@wraps preserves the original function's metadata like its name, docstring, and annotations. Without it, the decorated function would lose its identity, making debugging and documentation harder.

Can I use multiple decorators on one function? ▼

Yes! You can stack multiple decorators on one function. They run from bottom to top (closest to the function runs first). Each decorator adds its own behavior to the function.

Can I pass arguments to a decorator? ▼

Yes, by using a decorator factory — a function that takes arguments and returns a decorator. This allows you to customize the decorator's behavior, like specifying how many times to retry.

What are some common uses of decorators? ▼

Common uses include: timing function execution, logging function calls, authentication checks, caching results, rate limiting, and input validation. Flask and Django also use decorators for routing and permissions.

Where to Go From Here

Now that you understand decorators in Python, check out these related topics:

Property Decorator

Learn about Python's built-in @property decorator for getters and setters.

Learn More →

Generators

Learn about generators — another powerful Python feature.

Learn More →

Context Managers

Learn how context managers work and how to create them.

Learn More →
Interview Resources
  • Python Syntax & Variables Interview Questions
  • Top SQL Interview Questions & Answers
  • SQL Joins: Displaying Data from Multiple Tables FAQ
  • Python Lists and Dictionaries Interview Questions
  • Python OOP Interview Questions
  • SQL Set Operators Interview Questions
Previous: Generators Next: Type Hints →