Python Decorators: A Practical Guide

    Master Python decorators with practical examples for logging, authentication, caching, and more.

    11 min read
    Python
    Decorators
    Advanced
    Tutorial

    Introduction

    Decorators are one of the most powerful — and most misunderstood — features in Python.

    They allow you to:

    • ✔ Add functionality to existing functions without modifying their source code
    • ✔ Implement logging, authentication, caching, rate-limiting, and more
    • ✔ Write cleaner, more reusable, more professional code

    The best part?

    You can master decorators even as an intermediate Python programmer. This guide explains decorators clearly, with practical examples you can use immediately.

    1. What Is a Decorator in Python?

    A decorator is simply a function that takes another function and adds extra behaviour to it — without changing the original function's code.

    Think of it like wrapping a gift:

    • 🎁 gift = original function
    • 🎁 wrapping paper = decorator
    • 🎁 wrapped gift = enhanced function

    Basic structure:

    def decorator(func):
        def wrapper():
            print("Before")
            func()
            print("After")
        return wrapper

    Then you apply it using:

    @decorator
    def greet():
        print("Hello!")

    2. Why Use Decorators?

    Decorators are used everywhere in professional Python codebases because they help solve common problems:

    • Logging — Record when a function is called.
    • Authentication — Check if a user is authorized.
    • Speed Measurement — Find performance bottlenecks.
    • Retry Logic — Retry failed API calls.
    • Validation — Ensure function arguments are correct.
    • Caching (Memoization) — Return saved results for expensive functions.

    If you plan to build web apps, APIs, AI scripts, or automation — decorators will become your favourite tool.

    3. Your First Useful Decorator (Logging Example)

    Let's build a decorator that logs when a function is executed.

    def logger(func):
        def wrapper(*args, **kwargs):
            print(f"Running {func.__name__}...")
            result = func(*args, **kwargs)
            print(f"{func.__name__} finished.")
            return result
        return wrapper
    
    
    @logger
    def say_hello():
        print("Hello!")
    
    
    say_hello()

    Output:

    Running say_hello...
    Hello!
    say_hello finished.

    Just like that, you've added logging around the function — without touching its code.

    4. Decorators With Arguments

    Sometimes, you want the decorator itself to receive extra arguments.

    Example: rate-limiting a function.

    import time
    
    def delay(seconds):
        def decorator(func):
            def wrapper(*args, **kwargs):
                print(f"Waiting {seconds} seconds...")
                time.sleep(seconds)
                return func(*args, **kwargs)
            return wrapper
        return decorator
    
    
    @delay(2)
    def greet():
        print("Hello!")
    
    
    greet()

    This uses a three-level decorator, a common pattern:

    • outer → receives decorator arguments
    • middle → receives the function
    • inner → runs the function

    5. Practical Example: Measuring Function Speed

    Performance monitoring is essential in ML, data pipelines, and backend work.

    import time
    
    def timer(func):
        def wrapper(*args, **kwargs):
            start = time.time()
            result = func(*args, **kwargs)
            end = time.time()
            print(f"{func.__name__} ran in {end - start:.4f} seconds")
            return result
        return wrapper
    
    
    @timer
    def slow_function():
        time.sleep(1)
    
    
    slow_function()

    This decorator is used in real-world apps to identify slow functions.

    6. Using functools.wraps (Important!)

    Without functools.wraps, decorators break metadata:

    • docstrings
    • function names
    • IDE/autocomplete behaviour

    Correct version:

    from functools import wraps
    
    def logger(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"Calling {func.__name__}")
            return func(*args, **kwargs)
        return wrapper

    Always use @wraps in production code.

    7. Real-World Use Case: Authentication Decorator

    This is the type of decorator used in Flask, Django, and FastAPI.

    def require_login(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if not user.get("logged_in"):
                raise PermissionError("You must be logged in!")
            return func(user, *args, **kwargs)
        return wrapper
    
    
    @require_login
    def view_dashboard(user):
        return "Dashboard content"
    
    
    user = {"logged_in": True}
    print(view_dashboard(user))

    If user is not logged in → error. This pattern powers authentication systems across the web.

    8. Caching / Memoization Decorator (Performance Boost)

    Useful for expensive computations — like ML preprocessing, API calls, Fibonacci, etc.

    def cache(func):
        stored = {}
        
        @wraps(func)
        def wrapper(*args):
            if args in stored:
                return stored[args]
            result = func(*args)
            stored[args] = result
            return result
            
        return wrapper
    
    
    @cache
    def expensive_computation(n):
        print("Computing...")
        return n * n
    
    
    print(expensive_computation(10))  # Computes
    print(expensive_computation(10))  # Cached

    9. Stacking Multiple Decorators

    You can apply more than one:

    @timer
    @logger
    def run():
        print("Running task...")

    Order matters:

    • Logger wraps inner function
    • Timer wraps logger

    10. Summary

    By now, you should understand:

    • ✔ What decorators are
    • ✔ Why they're useful
    • ✔ How they wrap existing functions
    • ✔ How to add arguments to decorators
    • ✔ How to log, time, validate, authenticate
    • ✔ How to use functools.wraps
    • ✔ How real frameworks rely heavily on decorators

    Decorators are essential in:

    • Flask / Django / FastAPI routing
    • ML model caching
    • Logging systems
    • Database middleware
    • API request validation
    • Authentication layers

    Mastering decorators is a major step toward becoming a professional Python developer.

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service