📡 You're offline — showing cached content
New version available!
Quick Access
Python Intermediate

Python Decorators Explained: From Basics to Advanced Patterns

Master Python decorators — how they work, functools.wraps, decorators with arguments, class-based decorators, and practical examples like memoize and rate limiter.

EzyCoders Admin January 13, 2026 11 min read 1 views
Python Decorators Guide From Basics to Advanced
Share: Twitter LinkedIn WhatsApp

Python Decorators

Decorators are one of Python's most powerful features. They wrap a function to add behaviour without modifying its source code. Understanding them is essential for Python interviews and for writing clean, reusable code.

Functions Are First-Class Objects

def greet():
    return "Hello!"

# Functions can be assigned to variables
say_hi = greet
print(say_hi())  # Hello!

# Functions can be passed as arguments
def run(func):
    return func()

print(run(greet))  # Hello!

# Functions can be returned from other functions
def make_greeter(name):
    def inner():
        return f"Hello, {name}!"
    return inner  # return function, not its result

hello_rahul = make_greeter("Rahul")
print(hello_rahul())  # Hello, Rahul!

Your First Decorator

import time
from functools import wraps

def timer(func):
    @wraps(func)  # preserves original function metadata
    def wrapper(*args, **kwargs):
        start  = time.perf_counter()
        result = func(*args, **kwargs)  # call original
        end    = time.perf_counter()
        print(f"{func.__name__} took {end-start:.4f}s")
        return result
    return wrapper

@timer  # equivalent to: slow_function = timer(slow_function)
def slow_function():
    time.sleep(0.1)
    return "done"

slow_function()  # slow_function took 0.1001s

Decorator with Arguments

def retry(max_attempts=3, exceptions=(Exception,)):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    print(f"Attempt {attempt} failed: {e}")
                    if attempt == max_attempts:
                        raise
        return wrapper
    return decorator

@retry(max_attempts=3, exceptions=(ConnectionError,))
def fetch_data(url):
    # simulated network call
    raise ConnectionError("Network down")

# fetch_data("https://api.example.com")
# Attempt 1 failed: Network down
# Attempt 2 failed: Network down
# Attempt 3 failed: Network down
# raises ConnectionError

Practical Decorators

from functools import wraps

# 1. Login Required
def login_required(func):
    @wraps(func)
    def wrapper(request, *args, **kwargs):
        if not request.get('user'):
            return {'error': 'Login required', 'code': 401}
        return func(request, *args, **kwargs)
    return wrapper

# 2. Cache / Memoize
def memoize(func):
    cache = {}
    @wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fib(n):
    if n <= 1: return n
    return fib(n-1) + fib(n-2)

print(fib(50))  # instant

# 3. Rate Limiter
import time
from collections import defaultdict

def rate_limit(calls_per_second=1):
    last_called = defaultdict(float)
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - last_called[func.__name__]
            if elapsed < 1.0 / calls_per_second:
                time.sleep(1.0 / calls_per_second - elapsed)
            last_called[func.__name__] = time.time()
            return func(*args, **kwargs)
        return wrapper
    return decorator

# 4. Type Validation
def validate_types(**type_map):
    def decorator(func):
        @wraps(func)
        def wrapper(**kwargs):
            for param, expected in type_map.items():
                if param in kwargs and not isinstance(kwargs[param], expected):
                    raise TypeError(f"{param} must be {expected.__name__}")
            return func(**kwargs)
        return wrapper
    return decorator

@validate_types(name=str, age=int)
def create_user(name, age):
    return {'name': name, 'age': age}

create_user(name='Rahul', age=25)   # OK
create_user(name='Rahul', age='25') # TypeError: age must be int

Class-Based Decorators

class CountCalls:
    def __init__(self, func):
        self.func  = func
        self.count = 0
        wraps(func)(self)  # copy metadata

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} called {self.count} times")
        return self.func(*args, **kwargs)

@CountCalls
def process():
    return "processed"

process()  # process called 1 times
process()  # process called 2 times
print(process.count)  # 3

Q: Why use @wraps in decorators?

Without @wraps, the wrapped function loses its __name__, __doc__, and other metadata. If you call help() or inspect the function, you see the wrapper's metadata instead. @wraps(func) copies the original function's attributes to the wrapper.


Q: What is the execution order of stacked decorators?

Decorators apply bottom-up at definition time but execute top-down at call time. @a @b def f(): pass is equivalent to f = a(b(f)). When called, a's wrapper runs first, then b's wrapper, then the original function.

EzyCoders Admin
Written by
EzyCoders Admin

Team Lead and Full-Stack Developer with experience in PHP, JavaScript, SQL, DSA, and System Design. Passionate about software engineering, scalable web technologies, and helping developers prepare for coding interviews and tech careers through practical tutorials and professional guidance.

Comments (0)

No comments yet. Be the first!

Leave a Comment