Python Decorators
The @ Power Tool
Master Python's most elegant feature — decorators. From simple function wrappers to factory patterns, class decorators, and real‑world use cases. Write cleaner, more powerful code.
import functools, time def timer(func): @functools.wraps(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!" print(slow_function())
slow_function took 1.0012s
Done!
📑 Table of Contents
Jump to any section instantly
What Are Decorators?
Functions that modify other functions — elegantly
A decorator is a function that takes another function as an argument, wraps it with additional behaviour, and returns the wrapped function. Decorators let you add logging, timing, caching, authentication, and much more — without cluttering your core logic. They are one of Python's most powerful and distinctive features.
The @ Syntax
@decorator above a function is syntactic sugar for func = decorator(func). Decorators are applied at definition time and can be stacked (applied from bottom to top).
Basics & @ Syntax
Understanding closures, wrappers, and the decorator pattern
🔧 A Simple Decorator — Without @
Before the @ sugar, a decorator is just a function that returns a wrapped version of the original. Notice the closure — the inner wrapper captures func.
def uppercase_decorator(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result.upper() return wrapper def greet(): return "hello world" greet = uppercase_decorator(greet) print(greet())
wrapper function is a closure — it "remembers" func even after uppercase_decorator returns. This is the foundation of all decorators.✨ The @ Syntax — Cleaner and Readable
Using the @ symbol achieves the same result with less boilerplate. The decorator is applied when the function is defined.
def uppercase_decorator(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result.upper() return wrapper @uppercase_decorator def greet(): return "hello world" print(greet())
📝 Preserving Metadata — @functools.wraps
Without wraps, the decorated function loses its original name, docstring, and annotations. Always use @functools.wraps to preserve introspection.
import functools def logger(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper @logger def add(a: int, b: int) -> int: """Add two numbers.""" return a + b print(add.__name__) print(add.__doc__) print(add(3, 4))
Add two numbers.
Calling add
7
@functools.wraps, add.__name__ would show wrapper. This breaks debugging, documentation, and many frameworks (Flask, pytest, etc.).Advanced Decorator Patterns
Factories, stacking, class decorators, and parameterized decorators
🏭 Decorator Factories — Parameterized Decorators
A decorator factory is a function that returns a decorator. This allows you to pass arguments to the decorator itself (e.g., @retry(times=3)).
import functools, time def retry(times: int = 3, delay: float = 1.0): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, times + 1): try: return func(*args, **kwargs) except Exception as e: if attempt == times: raise print(f"Attempt {attempt} failed: {e}. Retrying...") time.sleep(delay) return wrapper return decorator @retry(times=2, delay=0.3) def unstable(): import random if random.random() < 0.5: raise ValueError("Random failure!") return "Success!" print(unstable())
Success!
@app.route('/')), pytest fixtures (@pytest.fixture(scope='session')), and more.📚 Stacking Decorators
You can apply multiple decorators to a single function. They are applied from bottom to top (the one closest to def runs first).
import functools def bold(func): @functools.wraps(func) def wrapper(*args, **kwargs): return "<b>" + func(*args, **kwargs) + "</b>" return wrapper def italic(func): @functools.wraps(func) def wrapper(*args, **kwargs): return "<i>" + func(*args, **kwargs) + "</i>" return wrapper @bold @italic def message(): return "Hello" print(message())
@bold wraps @italic which wraps message(). The output is <b><i>Hello</i></b>.🏗️ Class Decorators
Decorators work on classes too. A class decorator receives a class and returns a (possibly modified) class — great for adding attributes, methods, or registering classes.
def add_repr(cls): def __repr__(self): return f"{cls.__name__}({self.__dict__})" cls.__repr__ = __repr__ return cls @add_repr class Point: def __init__(self, x, y): self.x, self.y = x, y p = Point(3, 4) print(p)
Real‑World Decorator Use Cases
What professional Python developers actually use decorators for
⏱️ Timing and Performance Monitoring
Measure execution time, log slow functions, and monitor performance in production.
import functools, time, logging logging.basicConfig(level=logging.INFO) def timed(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start logging.info(f"{func.__name__} took {elapsed:.4f}s") return result return wrapper @timed def process_data(n: int): return sum(range(n)) print(process_data(1_000_000))
499999500000
🗄️ Caching with @lru_cache
Python's built-in functools.lru_cache is a decorator that memoizes function results — essential for expensive or recursive computations.
from functools import lru_cache @lru_cache(maxsize=128) def fibonacci(n: int) -> int: if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(35)) print(f"Cache info: {fibonacci.cache_info()}")
Cache info: CacheInfo(hits=33, misses=36, maxsize=128, currsize=36)
@lru_cache is one of Python's most useful built-in decorators. It caches results based on arguments, dramatically speeding up recursive and repeated calls.🔒 Authentication and Authorization
Decorators are the standard way to protect routes in web frameworks like Flask, Django, and FastAPI.
import functools def require_admin(func): @functools.wraps(func) def wrapper(user, *args, **kwargs): if not user.get("is_admin", False): raise PermissionError("Admin privileges required") return func(user, *args, **kwargs) return wrapper @require_admin def delete_user(user, user_id: int): return f"User {user_id} deleted" admin = {"name": "Alice", "is_admin": True} guest = {"name": "Bob", "is_admin": False} print(delete_user(admin, 42)) try: print(delete_user(guest, 42)) except PermissionError as e: print(f"Error: {e}")
Error: Admin privileges required
@login_required in Flask, @permission_required in Django, and @Depends in FastAPI work under the hood.Decorator FAQ
Quick answers to the most common decorator questions
What is the difference between a decorator and a decorator factory?
+A decorator is a function that takes a function and returns a function. A decorator factory is a function that returns a decorator — it allows you to pass parameters to the decorator (e.g., @retry(times=3)).
Why do I need @functools.wraps?
+Without wraps, the decorated function loses its original name (__name__), docstring (__doc__), annotations, and module. This breaks debugging, documentation tools, and frameworks. Always use @functools.wraps.
Can decorators be stacked?
+Yes. Multiple decorators are applied from bottom to top (closest to def runs first). For example, @bold @italic def message() applies italic first, then bold.
Do decorators work on methods and classes?
+Yes. Decorators work on any callable — functions, methods, classes, and even generator functions. Class decorators receive a class and can modify or replace it.
What are some common built‑in decorators?
+@staticmethod, @classmethod, @property, @functools.lru_cache, @dataclass, and @contextlib.contextmanager. Also @wraps (for writing decorators) and @total_ordering.
📬 Python, weekly
Short lessons, real code, and ecosystem news — every week