🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
Search courses…
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Python Decorators — Function Enhancement Patterns

Python AdvancedDecorators🟢 Free Lesson

Advertisement

Python Decorators — Function Enhancement Patterns

Decorators modify or enhance functions without changing their code. They are used extensively in frameworks, testing, logging, and caching.

Learning Objectives

  • Understand how decorators work under the hood
  • Create decorators with and without arguments
  • Apply decorators to classes and methods
  • Use common decorator patterns in production
  • Build a decorator library for real-world use

Basic Decorator

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "done"

slow_function()  # slow_function took 1.0012s

How It Works

# @syntax is equivalent to:
def slow_function():
    time.sleep(1)
    return "done"

slow_function = timer(slow_function)  # Decorator applied manually

# The decorated function is now 'wrapper'
print(slow_function.__name__)  # "wrapper" — metadata lost!

Preserving Function Metadata

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name):
    """Greet someone."""
    return f"Hello, {name}!"

print(greet.__name__)  # "greet" (not "wrapper")
print(greet.__doc__)   # "Greet someone."
print(greet.__wrapped__)  # Original function reference

# functools.wraps copies: __name__, __doc__, __module__, __dict__, __wrapped__

Decorator with Arguments

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

@retry(max_attempts=3, delay=0.5)
def fetch_data(url):
    import random
    if random.random() < 0.7:
        raise ConnectionError("Network error")
    return {"status": "ok"}

Alternative: functools.partial

from functools import partial

def retry(func=None, *, max_attempts=3, delay=1):
    if func is None:
        return partial(retry, max_attempts=max_attempts, delay=delay)

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        for attempt in range(1, max_attempts + 1):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                if attempt == max_attempts:
                    raise
                time.sleep(delay)
    return wrapper

# Works with or without arguments
@retry
def func1(): pass

@retry(max_attempts=5)
def func2(): pass

Class Decorators

def singleton(cls):
    instances = {}
    @functools.wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    def __init__(self):
        self.connection = "connected"

db1 = Database()
db2 = Database()
print(db1 is db2)  # True

# Class decorator that adds methods
def add_repr(cls):
    def __repr__(self):
        attrs = ', '.join(f'{k}={v!r}' for k, v in self.__dict__.items())
        return f'{cls.__name__}({attrs})'
    cls.__repr__ = __repr__
    return cls

@add_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
print(p)  # Point(x=1, y=2)

Stacking Decorators

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper

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

print(greet("Alice"))  # <b><i>Hello, Alice</i></b>

# Execution order: bold wraps italic wraps greet
# bold(italic(greet))

Common Production Decorators

# Caching (built-in since Python 3.8)
from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Cache info
print(fibonacci.cache_info())

# Deprecation warning
import warnings
def deprecated(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        warnings.warn(f"{func.__name__} is deprecated", DeprecationWarning, stacklevel=2)
        return func(*args, **kwargs)
    return wrapper

@deprecated
def old_function():
    pass

# Type checking decorator
def type_check(*types, return_type=None):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args):
            for arg, expected in zip(args, types):
                if not isinstance(arg, expected):
                    raise TypeError(f"Expected {expected}, got {type(arg)}")
            result = func(*args)
            if return_type and not isinstance(result, return_type):
                raise TypeError(f"Return type expected {return_type}, got {type(result)}")
            return result
        return wrapper
    return decorator

@type_check(int, int, return_type=int)
def add(a, b):
    return a + b

# Input validation
def validate(**validators):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for key, validator in validators.items():
                if key in kwargs:
                    if not validator(kwargs[key]):
                        raise ValueError(f"Invalid value for {key}: {kwargs[key]}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate(age=lambda x: x >= 0, name=lambda x: len(x) > 0)
def create_person(name, age):
    return {"name": name, "age": age}

Real-World: Decorator Library

import functools
import time
import logging
from typing import Callable, Any

logger = logging.getLogger(__name__)

def timer(func: Callable) -> Callable:
    """Measure execution time."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        logger.info(f"{func.__name__} completed in {elapsed:.4f}s")
        return result
    return wrapper

def retry(max_attempts: int = 3, delay: float = 1.0,
          exceptions: tuple = (Exception,)) -> Callable:
    """Retry on failure."""
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        raise
                    logger.warning(f"Attempt {attempt} failed: {e}. Retrying...")
                    time.sleep(delay)
        return wrapper
    return decorator

def cache(ttl: float = 300) -> Callable:
    """Cache with time-to-live expiration."""
    def decorator(func: Callable) -> Callable:
        cache_dict = {}
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            key = (args, tuple(sorted(kwargs.items())))
            now = time.time()
            if key in cache_dict:
                result, timestamp = cache_dict[key]
                if now - timestamp < ttl:
                    return result
            result = func(*args, **kwargs)
            cache_dict[key] = (result, now)
            return result
        wrapper.cache_clear = cache_dict.clear
        return wrapper
    return decorator

def rate_limit(calls_per_second: float) -> Callable:
    """Rate limit function calls."""
    min_interval = 1.0 / calls_per_second
    last_called = [0.0]

    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - last_called[0]
            if elapsed < min_interval:
                time.sleep(min_interval - elapsed)
            last_called[0] = time.time()
            return func(*args, **kwargs)
        return wrapper
    return decorator

# Usage
@timer
@retry(max_attempts=3, delay=0.5)
@cache(ttl=60)
def fetch_user(user_id: int) -> dict:
    return {"id": user_id, "name": f"User_{user_id}"}

Method Decorators

class MyClass:
    @staticmethod
    def static_method():
        """No access to class or instance."""
        return "static"

    @classmethod
    def class_method(cls):
        """Access to class, not instance."""
        return cls.__name__

    @property
    def value(self):
        """Computed attribute."""
        return self._value

    @value.setter
    def value(self, val):
        self._value = val

    @functools.lru_cache(maxsize=32)
    def expensive_method(self, n):
        """Cached method (note: uses self as key)."""
        return n ** 2

# Custom method decorator
def log_method_call(func):
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        logger.info(f"Calling {func.__name__} on {self.__class__.__name__}")
        result = func(self, *args, **kwargs)
        logger.info(f"{func.__name__} returned {result}")
        return result
    return wrapper

class Service:
    @log_method_call
    def process(self, data):
        return data * 2

Common Mistakes

# Mistake 1: Forgetting @functools.wraps
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def my_func():
    """My docstring."""
    pass

print(my_func.__name__)  # "wrapper" — lost metadata!

# Fix: always use @functools.wraps
def good_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# Mistake 2: Not handling *args and **kwargs
def bad_decorator(func):
    def wrapper(a, b):  # Only works for 2 positional args!
        return func(a, b)
    return wrapper

# Fix: use *args and **kwargs
def good_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# Mistake 3: Decorator with arguments confusion
# @retry  # Calls retry(func) — works if func is callable
# def func(): pass

# @retry(3)  # Calls retry(3) — returns decorator, then calls decorator(func)
# def func(): pass

# Fix: use the dual-mode pattern shown earlier

# Mistake 4: Decorator modifies class incorrectly
# class Decorator:
#     def __call__(self, func):
#         @functools.wraps(func)
#         def wrapper(*args, **kwargs):
#             return func(*args, **kwargs)
#         return wrapper

# @Decorator  # This replaces the class with 'wrapper' function!
# class MyClass: pass

Key Takeaways

  1. Decorators wrap functions to add behavior
  2. Always use @functools.wraps to preserve metadata
  3. Decorator factories use nested functions for arguments
  4. @lru_cache is built-in caching
  5. Common patterns: retry, timer, type check, deprecation, singleton
  6. Use *args, **kwargs in wrapper functions for flexibility
  7. Class decorators can modify classes, not just functions

Premium Content

Python Decorators — Function Enhancement Patterns

Unlock this lesson and 900+ advanced tutorials with a Premium plan.

🎯End-to-end Projects
💼Interview Prep
📜Certificates
🤝Community Access

Already a member? Log in

Need Expert Python Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement