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.
Python code
42 linesimport 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
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
- Add exponential backoff with a growth factor: delay *= 2 after each failure
- 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
More from Reliability & rate limiting
- At Least Once with Idempotent Consumer in Python medium
- Build a Rate Limiter Decorator in Python easy
- Build a queue-based admission control system in Python easy
- Chaos Inject Random Failures in Python easy
- Circuit breaker failure threshold count in Python medium
- Exactly Once Processing Dedupe Mock in Python easy
Keep learning
Related tutorials and quizzes for this topic.