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.

Easy Python 3.9+ Aug 9, 2026 Reliability & rate limiting 15 views 0 copies

Python code

38 lines
Python 3.9+
import 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

stdout
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

  1. Use exponential backoff: `delay = delay * 2` inside the loop
  2. 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

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.