Python Decorators Demystified

Learn how to enhance functions with elegant syntax and powerful functionality

What Are Python Decorators?

Decorators are a powerful feature in Python that allow you to modify or extend the behavior of functions or classes without permanently changing their code. They provide a clean, reusable way to add functionality like logging, timing, access control, and more.

Think of decorators as "wrappers" that you can easily apply to your functions using the @decorator_name syntax above your function definition.

This guide will walk you through practical decorator patterns you can use in your projects today!

Timing Functions

@timer Decorator

Measure execution time of any function with minimal code changes.

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end-start:.4f}s")
        return result
    return wrapper

@timer
def calculate_sum(n):
    return sum(range(n))

calculate_sum(1000000)
calculate_sum took 0.0421s

Real-world Use:

Performance monitoring, identifying bottlenecks in data processing pipelines

Function Logging

@log_calls Decorator

Automatically log function calls with arguments and return values.

import functools

def log_calls(func):
    @functools.wraps(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

@log_calls
def multiply(a, b):
    return a * b

multiply(5, 3)
Calling multiply with args=(5, 3), kwargs={}
multiply returned 15

Real-world Use:

Debugging complex workflows, auditing critical operations in financial applications

Retry Logic

@retry Decorator

Automatically retry failed operations with exponential backoff.

import time
import random

def retry(max_attempts=3, delay=1):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise e
                    time.sleep(delay * (2 ** attempt))
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def unstable_api_call():
    if random.random() < 0.7:
        raise ConnectionError("API down")
    return "Success!"

Real-world Use:

Handling network requests, database connections, and external API calls