How to Retry on Specific Exception Tuples in Python
A decorator-based retry pattern that retries a function only when it raises exceptions specified in a tuple, with configurable retries and delay.
Python code
38 linesimport time
import random
from unittest.mock import patch
def retry_on_exceptions(retries=3, exceptions=(ValueError,), delay=0.1):
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(retries):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == retries - 1:
raise
time.sleep(delay)
return wrapper
return decorator
def flaky_service():
if random.random() < 0.6:
raise ValueError("Temporary failure")
return "Success"
@retry_on_exceptions(retries=3, exceptions=(ValueError,), delay=0.05)
def critical_call():
return flaky_service()
if __name__ == "__main__":
with patch("random.random", side_effect=[0.2, 0.8]):
result = critical_call()
print(f"Result: {result}")
with patch("random.random", side_effect=[0.1, 0.3, 0.5, 0.9]):
result = critical_call()
print(f"Result: {result}")
Output
Result: Success
Result: Success
How it works
The retry_on_exceptions decorator wraps a function and attempts to call it up to retries times. It catches only the exceptions listed in the exceptions tuple. If the exception occurs on the last attempt, it re-raises the original error rather than silently swallowing it. The time.sleep(delay) between attempts provides a brief pause before retrying. This pattern is useful because it lets you target transient failures (like ValueError) while letting other exceptions propagate immediately. In the example, mocking random.random forces the flaky service to fail first, then succeed on a retry.
Common mistakes
- Forgetting to re-raise the exception on the final attempt, letting failures go silent
- Catching too broad an exception type, which masks permanent failures as transient
- Not including a delay between retries, causing tight retry loops under load
Variations
- Use exponential backoff: `delay = delay * 2` inside the loop
- Add `jitter` by using `random.uniform(0, delay)` for randomized delays
Real-world use cases
- Retrying a database write that often fails with a lock timeout but succeeds on a retry
- Re-attempting an external API call that occasionally returns a 500 but succeeds shortly after
- Retrying file operations (e.g., reading a file being written by another process) with a short wait
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.