Fundamentals 8 min read

Understanding Python Decorators: Concepts and Practical Examples

Python decorators are powerful constructs that let you augment functions without altering their definitions, and this guide explains their underlying principle, provides a basic template, and demonstrates ten practical decorator implementations—including timing, logging, access control, caching, singleton, retry, async, and property restrictions.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
Understanding Python Decorators: Concepts and Practical Examples

In Python, decorators are a powerful feature that allows users to add extra functionality to functions without modifying the original function definition. A decorator is essentially a function that takes another function as an argument and returns a new function, typically executing additional operations before or after the original function.

Basic decorator template:

def my_decorator(func):
    """
    This is a decorator template
    Parameters:
    func: the function to be decorated
    Returns:
    wrapper: the wrapped function
    """
    def wrapper(*args, **kwargs):
        """
        Internal function that wraps the original call
        Parameters:
        *args: positional arguments for the original function
        **kwargs: keyword arguments for the original function
        """
        # Add decorator functionality here, e.g., logging, timing, etc.
        print("Executing some operations before the function call...")
        # Call the original function
        result = func(*args, **kwargs)
        # Additional decorator functionality after the call
        print("Executing some operations after the function call...")
        return result
    # Return the wrapped function
    return wrapper

@my_decorator
def example_function(x):
    """Example function"""
    print(f"Function executed, received argument: {x}")

example_function("Hello, decorator!")

1. Performance testing decorator

import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} execution time: {end_time - start_time} seconds")
        return result
    return wrapper

@timing_decorator
def long_running_function(n):
    sum = 0
    for i in range(n):
        sum += i
    return sum

print(long_running_function(1000000))

2. Logging decorator

def logging_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"{func.__name__} called with args: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} finished, result: {result}")
        return result
    return wrapper

@logging_decorator
def add(a, b):
    return a + b

print(add(3, 5))

3. Permission check decorator

def admin_required(func):
    def wrapper(*args, **kwargs):
        user_role = "admin"  # Assume this is obtained from somewhere
        if user_role != "admin":
            raise Exception("Admin privileges required")
        return func(*args, **kwargs)
    return wrapper

@admin_required
def delete_user(user_id):
    print(f"Deleting user with ID {user_id}")

try:
    delete_user(123)
except Exception as e:
    print(e)

4. Simple caching decorator

cache = {}

def cache_decorator(func):
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@cache_decorator
def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(10))

5. Class method decorator

class Counter:
    count = 0
    @classmethod
    def increment(cls):
        cls.count += 1
        print(f"Current counter value: {cls.count}")

Counter.increment()
Counter.increment()

6. Singleton pattern decorator

def singleton(cls):
    _instance = {}
    def inner():
        if cls not in _instance:
            _instance[cls] = cls()
        return _instance[cls]
    return inner

@singleton
class MyClass:
    pass

a = MyClass()
 b = MyClass()
print(a is b)  # Should output True

7. Retry decorator (for network requests, etc.)

import time

def retry_on_failure(max_retries=3, delay=1):
    def decorator(func):
        def wrapper(*args, **kwargs):
            retries = 0
            while retries < max_retries:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Attempt {retries+1} failed, reason: {e}")
                    retries += 1
                    time.sleep(delay)
            raise Exception(f"Failed after {max_retries} attempts")
        return wrapper
    return decorator

@retry_on_failure(max_retries=3)
def might_fail(some_condition):
    if some_condition:
        raise ValueError("Operation failed")
    print("Operation succeeded")

might_fail(True)  # Will attempt 3 times

8. Argument validation decorator

def validate_args(min_value, max_value):
    def decorator(func):
        def wrapper(value):
            if not (min_value <= value <= max_value):
                raise ValueError(f"Value must be between {min_value} and {max_value}")
            return func(value)
        return wrapper
    return decorator

@validate_args(0, 100)
def process_value(value):
    print(f"Processing value: {value}")

process_value(50)  # Normal
# process_value(150)  # Would raise an exception

9. Asynchronous support decorator

import asyncio

async def async_decorator(func):
    async def wrapper(*args, **kwargs):
        print("Starting async operation...")
        result = await func(*args, **kwargs)
        print("Async operation completed")
        return result
    return wrapper

@async_decorator
async def fetch_data():
    print("Fetching data...")
    await asyncio.sleep(1)  # Simulate async work
    print("Data fetch complete")
    return "data"

asyncio.run(fetch_data())

10. Class property decorator (read‑only attribute)

def readonly_property(func):
    attr_name = "_{}".format(func.__name__)
    @property
    def wrapper(self):
        return getattr(self, attr_name)
    @wrapper.setter
    def wrapper(self, value):
        raise AttributeError("Attribute is read‑only, cannot set")
    setattr(wrapper, "__doc__", func.__doc__)
    return wrapper

class MyClass:
    @readonly_property
    def value(self):
        """This property is read‑only"""
        return 42

obj = MyClass()
print(obj.value)  # Outputs 42
# obj.value = 100  # Would raise AttributeError
programmingTutorialdecoratorcode
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

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.