Fundamentals 8 min read

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.

Code Mala Tang
Code Mala Tang
Code Mala Tang
Boost Your Python Code: 5 Powerful Custom Decorators You Must Use

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.

performancePythonbest practicescode reusedecorators
Code Mala Tang
Written by

Code Mala Tang

Read source code together, write articles together, and enjoy spicy hot pot together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.