How to Cap Retry Attempts in Python with a Decorator

Build a reusable retry decorator that caps attempts, adds delays, and lets flaky services fail fast instead of hanging.

Medium Python 3.9+ Aug 9, 2026 Reliability & rate limiting 13 views 0 copies

Python code

42 lines
Python 3.9+
import random
from functools import wraps
from time import sleep


def retry(max_attempts, delay=0.1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    if attempts == max_attempts:
                        raise
                    sleep(delay)
            return None
        return wrapper
    return decorator


class UnstableService:
    def __init__(self, fail_probability=0.4):
        self.fail_probability = fail_probability

    @retry(max_attempts=3)
    def fetch(self):
        if random.random() < self.fail_probability:
            raise ConnectionError("Temporary network failure")
        return "data"


if __name__ == "__main__":
    service = UnstableService()
    for i in range(5):
        try:
            result = service.fetch()
            print(f"Attempt {i+1}: success - {result}")
        except Exception as e:
            print(f"Attempt {i+1}: failed after retries - {e}")

Output

stdout
Attempt 1: success - data
Attempt 2: success - data
Attempt 3: failed after retries - Temporary network failure
Attempt 4: success - data
Attempt 5: success - data

How it works

The @retry(max_attempts=3) decorator wraps fetch so every call is retried up to three times before raising. The inner while loop increments attempts after each exception and calls sleep(delay) between tries, adding backoff. @wraps(func) preserves the original function's metadata, so debugging and introspection still work. When the cap is reached, the last exception is re-raised so callers can handle total failure explicitly.

Common mistakes

  • Catching too broad an exception and hiding permanent bugs as retryable failures
  • Forgetting to re-raise after the max attempts, causing silent None returns
  • Using the same delay forever instead of backing off for long-running services

Variations

  1. Add exponential backoff with a growth factor: delay *= 2 after each failure
  2. Catch only specific exception types by accepting a tuple of expected exceptions

Real-world use cases

  • Wrapping HTTP calls to third-party APIs that transiently return 5xx or network timeouts.
  • Retrying database writes when connection pools briefly exhaust without hanging production jobs.
  • Adding resilience to message consumers so temporary broker hiccups don't lose events.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Reliability & rate limiting

Related tutorials and quizzes for this topic.