Cleaning Up Python Code with Decorators: From ‘Copy-Paste’ Nightmares to Professional Programming

Python tutorial - IT technology blog
Python tutorial - IT technology blog

The ‘Copy-Paste’ Nightmare of Boilerplate Logic

In 2022, I managed a monitoring system with about 45 different automation scripts. These scripts performed health checks on everything from Databases and Redis to Load Balancers. At the time, the requirement was to measure performance: every function had to log its start time, end time, and total execution time.

When I was starting out, I chose the most manual approach. I went into every function and inserted time.time() at the beginning and end. The result was 45 identical snippets of code scattered everywhere:

import time

def check_database_health():
    start = time.time()
    # Complex DB check logic
    print("Checking Database...")
    time.sleep(1)
    
    end = time.time()
    print(f"Execution time: {end - start}s")

def check_redis_health():
    start = time.time()
    # Redis check logic
    print("Checking Redis...")
    time.sleep(0.5)
    
    end = time.time()
    print(f"Execution time: {end - start}s")

Everything went south when the requirements changed. Instead of printing to the console, I had to switch to logging JSON to a file. Digging through 45 functions to make changes took me the whole afternoon and was extremely error-prone. This was when I realized I was seriously violating the DRY (Don’t Repeat Yourself) principle.

The Trap of Mixing Logics

The problem wasn’t just the effort. When you mix business logic with auxiliary logic like logging or auth, the code becomes hard to read. A colleague looking at the code would have to “swim” through a mess of timing calculations just to see the actual purpose of the function.

In engineering, this is known as Cross-cutting Concerns. These are features that appear across different layers of an application. If you find yourself repeating the same code structure at the start and end of multiple functions, it’s a sign that Decorators should step in.

The Solution: From Manual Wrappers to Professional Decorators

1. Using Wrapper Functions

Instead of modifying functions directly, I extracted the timing logic into a wrapper function. This approach helps separate code responsibilities:

def timer_wrapper(func):
    def wrapper():
        start = time.time()
        func()
        end = time.time()
        print(f"Time taken: {end - start}s")
    return wrapper

def my_task():
    print("Doing something...")

# Usage
wrapped_task = timer_wrapper(my_task)
wrapped_task()

Even though the logic was separated, calling wrapped_task() was still quite cumbersome. If you have hundreds of functions, manual assignment like this would be a management disaster.

2. Mastering the @Decorator Syntax

Python provides the @ syntax to solve this elegantly. A Decorator is essentially a function that takes another function as input and returns an “upgraded” version of it. It makes code cleaner, preserves the original function name, and is extremely easy to maintain.

Implementing Decorators Like a Senior Developer

To make a Decorator flexible for any function (with or without parameters), you should use *args and **kwargs. Here is the structure I always use in real-world projects:

import time
from functools import wraps

def logger_decorator(func):
    @wraps(func) # Preserve original metadata (function name, docstring)
    def wrapper(*args, **kwargs):
        print(f"--- Starting: {func.__name__} ---")
        start_time = time.time()
        
        result = func(*args, **kwargs) 
        
        end_time = time.time()
        print(f"--- Completed: {func.__name__} in {end_time - start_time:.4f}s ---")
        return result
    return wrapper

@logger_decorator
def sync_data(source, destination):
    """Simulating data synchronization between servers"""
    print(f"Transferring data from {source} to {destination}...")
    time.sleep(2)
    return "Success"

Important note: Always use @wraps(func). Without it, the sync_data.__name__ attribute would change to 'wrapper'. This makes debugging or automatic documentation generation extremely difficult.

3 Real-World Applications in DevOps and Backend

Decorators aren’t just for timing. In practice, I often apply them to these three scenarios:

  • Retry Logic: Automatically retry an API call 3-5 times if a network error occurs before reporting failure.
  • Role-Based Access Control (RBAC): Check for Admin permissions before executing sensitive commands like deleting a database.
  • Caching: Store the results of heavy queries in Redis, reducing DB load by up to 70% for repetitive tasks.

Example of an Access Control Decorator:

def require_admin(func):
    @wraps(func)
    def wrapper(user_role, *args, **kwargs):
        if user_role != "admin":
            print("Access Denied: You do not have permission to perform this action!")
            return None
        return func(user_role, *args, **kwargs)
    return wrapper

@require_admin
def delete_system_logs(user_role):
    print("Deleting system logs...")

Conclusion

Decorators aren’t some mystical, high-level technique. They represent a different way of thinking about code organization. Instead of writing code vertically (adding lines inside a function), we wrap it in layers.

If you find yourself rewriting the same auxiliary logic in more than three different functions, stop for five minutes. Turning it into a Decorator will save you hours of maintenance later. Your code will be professional, readable, and most importantly, scalable.

Share: