Skip to main content
Advertisement

Advanced Function Decorators

Decorators are a powerful Python feature that wrap functions or classes to add behavior without modifying the original code.


functools.wraps — Preserving Metadata

Applying a decorator loses the original function's metadata such as __name__ and __doc__. Use functools.wraps to preserve them.

import functools


# Decorator without wraps — metadata lost
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper


# Decorator with wraps — metadata preserved
def good_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper


def greet(name: str) -> str:
"""Returns a greeting."""
return f"Hello, {name}!"


bad_greet = bad_decorator(greet)
good_greet = good_decorator(greet)

print(bad_greet.__name__) # wrapper (original name lost)
print(good_greet.__name__) # greet (original name preserved)
print(bad_greet.__doc__) # None (docstring lost)
print(good_greet.__doc__) # Returns a greeting.

import inspect
print(inspect.signature(good_greet)) # (name: str) -> str

Parameterized Decorators

To pass arguments to the decorator itself, use a triple-nested function structure.

import functools
import time


def repeat(times: int):
"""Decorator that runs a function n times"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator


def retry(max_attempts: int = 3, delay: float = 0.5, exceptions: tuple = (Exception,)):
"""Decorator that retries on failure"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_attempts:
print(f"[Retry {attempt}/{max_attempts}] {func.__name__}: {e}")
time.sleep(delay)
raise last_exception
return wrapper
return decorator


@repeat(3)
def say_hello():
print("Hello!")

say_hello() # prints "Hello!" 3 times


@retry(max_attempts=3, delay=0.1, exceptions=(ValueError,))
def risky_parse(text: str) -> int:
import random
if random.random() < 0.5:
raise ValueError("Parse failed!")
return int(text)


try:
result = risky_parse("42")
print(f"Parse result: {result}")
except ValueError:
print("Max retries exceeded")

Decorator Factory Pattern

import functools
import time
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def log_calls(level: str = "INFO", include_args: bool = True, include_result: bool = True):
"""Decorator that logs function calls"""
log_fn = getattr(logger, level.lower())

def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if include_args:
log_fn(f"Call: {func.__name__}({args}, {kwargs})")
else:
log_fn(f"Call: {func.__name__}()")

start = time.perf_counter()
try:
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
if include_result:
log_fn(f"Done: {func.__name__}{result!r} ({elapsed:.4f}s)")
else:
log_fn(f"Done: {func.__name__} ({elapsed:.4f}s)")
return result
except Exception as e:
elapsed = time.perf_counter() - start
logger.error(f"Error: {func.__name__}{type(e).__name__}: {e} ({elapsed:.4f}s)")
raise
return wrapper
return decorator


def rate_limit(calls_per_second: float):
"""Decorator that limits call frequency"""
min_interval = 1.0 / calls_per_second
last_call_time = [0.0]

def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
now = time.monotonic()
elapsed = now - last_call_time[0]
if elapsed < min_interval:
sleep_time = min_interval - elapsed
time.sleep(sleep_time)
last_call_time[0] = time.monotonic()
return func(*args, **kwargs)
return wrapper
return decorator


@log_calls(level="INFO", include_args=True)
def add(a: int, b: int) -> int:
return a + b


add(3, 5)
add(10, 20)

Decorator Stacking (Combining Multiple Decorators)

import functools
import time


def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"[Timer] {func.__name__}: {time.perf_counter() - start:.4f}s")
return result
return wrapper


def validate_positive(*param_names: str):
"""Validates that specified parameters are positive"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for name in param_names:
if name in bound.arguments:
value = bound.arguments[name]
if isinstance(value, (int, float)) and value <= 0:
raise ValueError(f"{name} must be positive: {value}")
return func(*args, **kwargs)
return wrapper
return decorator


def memoize(func):
cache: dict = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper


# Decorator order: bottom to top (memoize → validate_positive → timer)
@timer
@validate_positive("n")
@memoize
def fibonacci(n: int) -> int:
"""Compute the nth Fibonacci number"""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)


print(fibonacci(10)) # 55
print(fibonacci(10)) # 55 (from cache, timer still runs)

try:
fibonacci(-1) # ValueError: n must be positive
except ValueError as e:
print(f"Validation error: {e}")


print(fibonacci.__name__) # fibonacci (preserved by wraps)

Implementing Decorators as Classes

import functools
import time


class Timer:
"""Class-based decorator — implements __call__"""

def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.call_count = 0
self.total_time = 0.0

def __call__(self, *args, **kwargs):
start = time.perf_counter()
result = self.func(*args, **kwargs)
elapsed = time.perf_counter() - start
self.call_count += 1
self.total_time += elapsed
print(f"[{self.func.__name__}] Call #{self.call_count}: {elapsed:.4f}s")
return result

@property
def average_time(self) -> float:
return self.total_time / self.call_count if self.call_count else 0

def stats(self) -> str:
return (f"{self.func.__name__}: "
f"{self.call_count} calls, "
f"avg {self.average_time:.4f}s, "
f"total {self.total_time:.4f}s")


@Timer
def slow_function(n: int) -> int:
time.sleep(0.01)
return n ** 2


for i in range(3):
slow_function(i)

print(slow_function.stats())
print(f"Call count: {slow_function.call_count}")


# Parameterized class-based decorator
class Retry:
def __init__(self, max_attempts: int = 3, exceptions: tuple = (Exception,)):
self.max_attempts = max_attempts
self.exceptions = exceptions

def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, self.max_attempts + 1):
try:
return func(*args, **kwargs)
except self.exceptions as e:
if attempt == self.max_attempts:
raise
print(f"Retry {attempt}/{self.max_attempts}: {e}")
return wrapper


@Retry(max_attempts=3, exceptions=(ConnectionError,))
def connect_to_server(host: str) -> str:
import random
if random.random() < 0.7:
raise ConnectionError(f"Failed to connect to {host}")
return f"Connected to {host}"


try:
print(connect_to_server("api.example.com"))
except ConnectionError as e:
print(f"Final failure: {e}")

Real-World Example: API Endpoint Decorators

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


def require_auth(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
token = kwargs.get("token") or (args[0] if args else None)
if not token or not token.startswith("Bearer "):
raise PermissionError("Authentication required.")
return func(*args, **kwargs)
return wrapper


def cache_with_ttl(ttl_seconds: int = 60):
def decorator(func: Callable) -> Callable:
_cache: dict[str, tuple[Any, float]] = {}

@functools.wraps(func)
def wrapper(*args, **kwargs):
key = hashlib.md5(
f"{args}{sorted(kwargs.items())}".encode()
).hexdigest()

now = time.monotonic()
if key in _cache:
value, expire_at = _cache[key]
if now < expire_at:
return value
del _cache[key]

result = func(*args, **kwargs)
_cache[key] = (result, now + ttl_seconds)
return result

wrapper.cache_clear = lambda: _cache.clear()
wrapper.cache_size = lambda: len(_cache)
return wrapper
return decorator


def json_response(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
data = func(*args, **kwargs)
return {"success": True, "data": data, "error": None}
except ValueError as e:
return {"success": False, "data": None, "error": str(e)}
except PermissionError as e:
return {"success": False, "data": None, "error": f"Permission error: {e}"}
return wrapper


@json_response
@cache_with_ttl(ttl_seconds=30)
@require_auth
def get_user_data(token: str, user_id: int) -> dict:
if user_id <= 0:
raise ValueError(f"Invalid user ID: {user_id}")
return {"id": user_id, "name": f"user_{user_id}", "email": f"user{user_id}@example.com"}


print(get_user_data("Bearer valid-token", 42))
print(get_user_data("invalid-token", 42))
print(get_user_data("Bearer valid-token", -1))

Pro Tips

1. Access the original function via __wrapped__

import functools


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


@my_decorator
def original():
"""Original function"""
return "original"


print(original.__wrapped__) # <function original at ...>
print(original.__wrapped__()) # original
print(original()) # original (passes through decorator)

2. Conditional decorator application

import functools
import os


def maybe_decorate(condition: bool, decorator):
"""Apply decorator if condition is True, otherwise return function as-is"""
def apply(func):
if condition:
return decorator(func)
return func
return apply


def debug_mode_only(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[DEBUG] {func.__name__} called")
return func(*args, **kwargs)
return wrapper


DEBUG = os.getenv("DEBUG", "false").lower() == "true"

@maybe_decorate(DEBUG, debug_mode_only)
def process_data(data: list) -> list:
return [x * 2 for x in data]


result = process_data([1, 2, 3])
print(result) # [2, 4, 6]

Summary

PatternUse CaseKey Point
Basic decoratorWrapping functionsfunctools.wraps is essential
Parameterized decoratorConfigurable behaviorTriple-nested function structure
Class-based decoratorWhen state needs to be maintainedImplement __call__
Decorator stackCombining multiple featuresWatch application order (bottom → top)

Always use functools.wraps. Omitting it makes debugging and introspection difficult.

Advertisement