Boost Your Python Code: 5 Powerful Custom Decorators You Must Use
This article explores how Python decorators can eliminate repetitive code, improve performance, enforce type safety, simplify debugging, and implement rate limiting, offering five custom decorator examples with clear explanations and ready-to-use implementations.
In programming, efficiency often depends on how elegantly we can reuse and extend code. Python decorators are a powerful tool that let developers modify or extend the behavior of functions and methods, yet many developers do not fully utilize or understand them.
“A language that does not change the way you think about programming is not worth learning.” – Alan Perlis
When you truly understand decorators, they can change the way you think about code structure. This article delves into five custom Python decorators that can improve project quality and simplify the programming experience.
Problem: Repeated Patterns in Code
During development, repetitive patterns such as logging, error handling, and performance monitoring often appear in multiple functions, increasing the risk of bugs and making maintenance harder.
Solution: Python Decorators
Decorators provide a concise and elegant solution. By wrapping additional functionality around a function, decorators reduce redundancy, promote code reuse, and improve readability.
1. Measure Execution Time
Tracking execution time for functions, especially during performance testing, can become tedious if done repeatedly.
Decorator: time_logger
<code>import time
def time_logger(func):
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
print(f"Function '{func.__name__}' took {end_time - start_time:.4f} seconds.")
return result
return wrapper
</code>Usage:
<code>@time_logger
def process_data(n):
time.sleep(n)
return f"Processed {n} seconds of data."
process_data(2)
</code>2. Retry Error‑Prone Functions
For functions that may raise transient errors (e.g., API calls), a retry decorator can encapsulate the retry logic.
Decorator: retry
<code>import time
def retry(retries=3, delay=1):
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(retries):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(delay)
raise Exception(f"Function '{func.__name__}' failed after {retries} retries.")
return wrapper
return decorator
</code>Usage:
<code>@retry(retries=5, delay=2)
def fetch_data():
import random
if random.random() < 0.8:
raise ValueError("Transient error!")
return "Data fetched successfully."
print(fetch_data())
</code>3. Enforce Type Checking
Python’s type hints are not enforced at runtime. This decorator ensures that arguments match expected types.
Decorator: type_check
<code>def type_check(*expected_types):
def decorator(func):
def wrapper(*args, **kwargs):
for arg, expected in zip(args, expected_types):
if not isinstance(arg, expected):
raise TypeError(f"Expected {expected}, got {type(arg)}")
return func(*args, **kwargs)
return wrapper
return decorator
</code>Usage:
<code>@type_check(int, int)
def add(a, b):
return a + b
print(add(2, 3)) # works
# print(add(2, "3")) # raises TypeError
</code>4. Debug Function Calls
A debugging decorator can log input arguments and return values, making troubleshooting easier.
Decorator: debug
<code>def debug(func):
def wrapper(*args, **kwargs):
print(f"Calling '{func.__name__}' with args: {args} kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"'{func.__name__}' returned: {result}")
return result
return wrapper
</code>Usage:
<code>@debug
def multiply(a, b):
return a * b
</code>5. Rate Limiting
When a function (e.g., an API request) should only run a limited number of times within a time window, a rate‑limiting decorator is valuable.
Decorator: rate_limiter
<code>import time
def rate_limiter(calls, period):
def decorator(func):
last_calls = []
def wrapper(*args, **kwargs):
nonlocal last_calls
now = time.time()
last_calls = [t for t in last_calls if now - t < period]
if len(last_calls) >= calls:
raise RuntimeError("Rate limit exceeded.")
last_calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
</code>Usage:
<code>@rate_limiter(calls=2, period=5)
def fetch_data():
print("Fetching data...")
fetch_data()
time.sleep(2)
fetch_data()
# fetch_data() # would raise RuntimeError
</code>Conclusion
Python decorators offer an elegant way to enhance and extend code functionality. They reduce redundancy, make codebases more modular and readable, and embody the principle that “programs are meant to be read by humans, with computers executing them occasionally.”
“Programs are meant to be read by humans, with computers executing them occasionally.” – Donald Knuth
Decorators embody this philosophy, making code cleaner and easier to maintain.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.