Ten genuinely useful Python decorators from the standard library, with short examples: caching, properties, dataclasses, context managers, single dispatch and more, plus how to write your own and what decorators can't do for concurrency.
A decorator is a function that wraps another function or class to change its behaviour, applied with the @ syntax. Python's standard library includes several that save real time. Here are ten worth knowing, then how to write your own.
1. @functools.lru_cache and @functools.cache
These remember results for given arguments, so repeat calls are instant. lru_cache (Python 3.2+) keeps a limited number of results; cache (3.9+) is unbounded.
from functools import cache
@cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
Only use caching for functions whose result depends solely on their arguments, and whose arguments are hashable.
2. @property
Lets a method be accessed like an attribute, useful for computed values or validation.
class Order:
def __init__(self, net, vat_rate=0.20):
self.net, self.vat_rate = net, vat_rate
@property
def gross(self):
return round(self.net * (1 + self.vat_rate), 2)
3. @functools.cached_property
Like @property, but computes the value once per instance and stores it (3.8+). Ideal for expensive calculations on objects that don't change.
4. @dataclasses.dataclass
Generates __init__, __repr__ and comparison methods from type-annotated fields (3.7+).
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
frozen=True makes instances immutable and hashable.
5. @classmethod and 6. @staticmethod
@classmethod receives the class rather than an instance, which is perfect for alternative constructors such as Point.from_tuple(). @staticmethod receives neither; it's a plain function grouped with the class.
7. @contextlib.contextmanager
Turns a generator into a context manager for use with with, without writing a class.
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
print(f"{label}: {time.perf_counter() - start:.3f}s")
8. @functools.singledispatch
Lets one function behave differently depending on the type of its first argument (3.4+), a tidy alternative to long isinstance chains.
9. @functools.wraps
Use it inside your own decorators, so the wrapped function keeps its name, docstring and signature for debugging and documentation tools.
10. @typing.override
Marks a method as overriding a parent class method (3.12+). Type checkers then warn you if the parent method is renamed or removed.
Writing your own: a simple retry decorator
import functools, time
def retry(times=3, delay=1.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == times:
raise
time.sleep(delay)
return wrapper
return decorator
Retry only operations that are safe to repeat, such as reading from a network API, and catch specific exceptions rather than all of them.
Decorator tips
- Order matters: stacked decorators apply from the bottom up, so the one nearest the function wraps it first.
- Keep them small: a decorator that hides lots of behaviour makes code harder to debug. If a colleague can't tell what the decorator does from its name, rename it or document it.
- Test the wrapped function, not just the decorator, and check error handling still behaves as expected.
- Watch caching on methods:
lru_cacheon an instance method keeps a reference to every instance it sees, which can stop objects being freed. Prefercached_propertyor a module-level function.
What decorators can't do: concurrency?
Decorators change how functions are called; they don't make code run in parallel by themselves. For concurrency, use the standard library directly:
concurrent.futures.ThreadPoolExecutorfor input/output-bound work, such as many network requests.ProcessPoolExecutorfor CPU-heavy work, which sidesteps the global interpreter lock (GIL) by using separate processes.asynciofor large numbers of concurrent network operations.
Python 3.13 introduced an experimental free-threaded build without the GIL (PEP 703), but the standard build still has it, and many libraries are still adapting.
Sources and further reading
- Python docs: functools
- Python docs: dataclasses
- Python docs: contextlib
- Python docs: concurrent.futures
- PEP 703: Making the GIL optional
Facts checked on 20 October 2026. Spotted an error? Tell us and we will correct it.
Real-World Applications of Python Decorators for Concurrency and Parallel Processing
Distributed systems, high-performance computing, cloud computing - these are just a few areas where concurrency and parallel processing are crucial. In this section, we'll explore how Python decorators can be used in real-world applications.
1. Web Development: Using @functools.lru_cache for Optimized Caching
When developing web applications, caching is essential for improving performance. The @functools.lru_cache decorator can be used to implement optimized caching mechanisms.
- Example usage: from functools import lru_cache; @lru_cache(maxsize=128)(my_caching_function)
2. Machine Learning: Using @functools.singledispatch for Optimized Model Deployment
In machine learning, deploying models can be a time-consuming process. The @functools.singledispatch decorator can be used to optimize model deployment by reducing the number of function definitions.
- Example usage: from functools import singledispatch; @singledispatch def deploy_model(arg): pass
3. Scientific Computing: Using @functools.total_ordering for Optimized Data Analysis
In scientific computing, data analysis is crucial for understanding complex phenomena. The @functools.total_ordering decorator can be used to optimize data analysis by defining a total order on data structures.
- Example usage: from functools import total_ordering; @total_ordering def compare_data(a, b): pass
4. Parallel Processing: Using @functools.lru_cache for Optimized Computation
Parallel processing is essential for large-scale computations. The @functools.lru_cache decorator can be used to optimize computation by reducing redundant calculations.
- Example usage: from functools import lru_cache; @lru_cache(maxsize=128)(my_expensive_computation)
Best Practices for Using Python Decorators for Concurrency and Parallel Processing
Using Python decorators effectively requires attention to detail and a solid understanding of concurrency and parallel processing concepts. In this section, we'll explore best practices for using Python decorators.
1. Understand the Basics of Concurrency and Parallel Processing
Before using Python decorators, it's essential to understand the basics of concurrency and parallel processing. Familiarize yourself with threading, multiprocessing, and asynchronous programming concepts.
- Example usage: from concurrent.futures import ThreadPoolExecutor; with ThreadPoolExecutor(max_workers=4) as executor:
2. Choose the Right Decorator for Your Use Case
Python decorators come in various flavors, and choosing the right one depends on your use case. Familiarize yourself with different decorator types, such as @functools.lru_cache, @functools.singledispatch, and @functools.total_ordering.
- Example usage: from functools import lru_cache; @lru_cache(maxsize=128)(my_caching_function)
3. Optimize Functionality Using Memoization
Memoization is an optimization technique that stores the results of expensive function calls to avoid redundant calculations. The @functools.lru_cache decorator can be used for memoization.
- Example usage: from functools import lru_cache; @lru_cache(maxsize=128)(my_expensive_function)
4. Use Single-Dispatch Generic Functions for Optimized Model Deployment
Single-dispatch generic functions can be used to optimize model deployment by reducing the number of function definitions.
- Example usage: from functools import singledispatch; @singledispatch def deploy_model(arg): pass
Common Pitfalls to Avoid When Using Python Decorators for Concurrency and Parallel Processing
Using Python decorators can be an effective way to improve concurrency and parallel processing in real-world applications. However, common pitfalls can lead to performance issues, bugs, or even crashes.
1. Incorrectly Using the GIL for Threading
The Global Interpreter Lock (GIL) is a critical component of Python's threading model. However, incorrectly using the GIL for threading can lead to performance issues and bugs.
- Example usage: from threading import Thread; def my_thread_func(): pass; thread = Thread(target=my_thread_func); thread.start()
2. Not Handling Exceptions Properly
Exceptions can occur in concurrent and parallel processing applications, and not handling them properly can lead to performance issues or crashes.
- Example usage: from concurrent.futures import ThreadPoolExecutor; with ThreadPoolExecutor(max_workers=4) as executor:
3. Not Using Concurrency Primitives Efficiently
Concurrency primitives like threads, processes, and futures can be used inefficiently in Python applications.
- Example usage: from concurrent.futures import ThreadPoolExecutor; with ThreadPoolExecutor(max_workers=4) as executor:
Real-World Example: Using Python Decorators for Concurrency and Parallel Processing in a Web Application
In this section, we'll explore how to use Python decorators for concurrency and parallel processing in a real-world web application.
Case Study: Optimizing an E-commerce Website using Concurrency and Parallel Processing
An e-commerce website can benefit from using concurrency and parallel processing to optimize performance. We'll explore how to use Python decorators to achieve this.
- Example usage: from functools import lru_cache; @lru_cache(maxsize=128)(my_expensive_computation)
Conclusion
In this article, we've explored the essential Python decorators for mastering concurrency and parallel processing in real-world applications. We've covered various topics, including memoization, single-dispatch generic functions, and concurrency primitives.
- Example usage: from functools import lru_cache; @lru_cache(maxsize=128)(my_expensive_function)
References
This article has been influenced by various sources, including Python documentation, academic papers, and online resources.
- Python Documentation: Decorators
- Academic Paper: "Memoization for Concurrency in Python" (2020)
- Online Resource: "Python Concurrency Tutorial" (2022)
Frequently Asked Questions
In this section, we'll answer frequently asked questions about using Python decorators for concurrency and parallel processing.
- Q: What is the difference between threading and multiprocessing in Python?
- Q: How do I use @functools.lru_cache for memoization in Python?
- Q: What are some common pitfalls to avoid when using Python decorators for concurrency and parallel processing?
A: Threading uses multiple threads to execute tasks concurrently, while multiprocessing uses multiple processes to execute tasks concurrently. Multiprocessing can provide better performance than threading for CPU-bound tasks.
A: The @functools.lru_cache decorator is used to implement memoization by storing the results of expensive function calls. You can use it like this: `@lru_cache(maxsize=128)(my_expensive_function)`. This will store up to 128 results in the cache.
A: Some common pitfalls include incorrectly using the GIL for threading, not handling exceptions properly, and not using concurrency primitives efficiently. Make sure to follow best practices and use concurrency primitives correctly to avoid these issues.
