How to Build a Robust Retry Decorator with Logging in Python

This article explains how to create a reusable Python decorator that automatically retries a function a configurable number of times, supports custom exception types and delays, preserves the original function's metadata, and optionally logs each attempt with adjustable log levels.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
How to Build a Robust Retry Decorator with Logging in Python

Feature Requirements

Automatically retry a function when it fails, up to N times.

Customizable retry count via max_retries (default 3).

Specify which exception types to catch (e.g., Exception or more specific).

Optional retry interval ( delay in seconds).

Preserve the original function's metadata using functools.wraps.

Full Code Implementation

import time
from functools import wraps

def retry(max_retries=3, delay=1, exceptions=(Exception,)):
    """Retry decorator.
    Args:
        max_retries (int): Maximum number of retries (default 3).
        delay (int or float): Seconds to wait between retries.
        exceptions (tuple): Exception types to catch and retry (default all Exception).
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            retries = 0
            while retries <= max_retries:
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    retries += 1
                    if retries > max_retries:
                        print(f"[Failed] Exceeded max retries {max_retries}, no more attempts")
                        raise
                    print(f"[Attempt {retries}/{max_retries}] Exception: {e}, retrying after {delay} seconds...")
                    time.sleep(delay)
            return None  # Should never reach here
        return wrapper
    return decorator

Example Usage

@retry(max_retries=5, delay=2, exceptions=(ConnectionError,))
def fetch_data():
    print("Fetching data...")
    # Simulate occasional failure
    import random
    if random.random() < 0.7:
        raise ConnectionError("Connection failed, please retry")
    return "Data fetched successfully"

result = fetch_data()
print(result)

Sample output:

Fetching data...

[Attempt 1/5] Exception: Connection failed, please retry, retrying after 2 seconds...

Fetching data...

[Attempt 2/5] Exception: Connection failed, please retry, retrying after 2 seconds...

Fetching data...

Data fetched successfully

Enhancement: Logging on Each Retry

Record detailed logs for every retry.

Support log level control (INFO / ERROR).

Optionally write logs to console or a file.

Retry Decorator with Logging

import time
import logging
from functools import wraps

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[logging.StreamHandler()]
)

def retry(max_retries=3, delay=1, exceptions=(Exception,)):
    """Retry decorator with logging.
    Args:
        max_retries (int): Maximum retry attempts.
        delay (int or float): Seconds between retries.
        exceptions (tuple): Exception types to catch.
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            retries = 0
            while retries <= max_retries:
                try:
                    logging.info(f"[{func.__name__}] Executing...")
                    result = func(*args, **kwargs)
                    logging.info(f"[{func.__name__}] Execution succeeded")
                    return result
                except exceptions as e:
                    retries += 1
                    logging.error(f"[{func.__name__}] Retry {retries} failed: {e}")
                    if retries > max_retries:
                        logging.error(f"[{func.__name__}] Exceeded max retries {max_retries}, aborting")
                        raise
                    logging.info(f"[{func.__name__}] Waiting {delay}s before retry {retries + 1}...")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

Logging‑Enabled Example

import random
@retry(max_retries=5, delay=2, exceptions=(ConnectionError,))
def fetch_data():
    print("Attempting to fetch data...")
    if random.random() < 0.7:
        raise ConnectionError("Simulated network outage")
    return "Data fetched successfully"

result = fetch_data()
print(result)

Sample log output:

2025-06-29 10:30:45,123 [INFO] [fetch_data] Executing...
Attempting to fetch data...
2025-06-29 10:30:45,125 [ERROR] [fetch_data] Retry 1 failed: Simulated network outage
2025-06-29 10:30:45,126 [INFO] [fetch_data] Waiting 2s before retry 2...
2025-06-29 10:30:47,130 [ERROR] [fetch_data] Retry 2 failed: Simulated network outage
2025-06-29 10:30:47,131 [INFO] [fetch_data] Waiting 2s before retry 3...
2025-06-29 10:30:49,135 [INFO] [fetch_data] Execution succeeded
Data fetched successfully
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backendPythonloggingretryerror handlingDecorator
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

0 followers
Reader feedback

How this landed with the community

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.