Reusable Python Retry and Backoff Decorator Library Prompt
Design a small, reusable retry/backoff decorator that wraps flaky functions with exponential backoff, jitter, exception filtering, and hooks — for both sync and async code.
- Target user
- Python developers hardening flaky calls with a reusable retry primitive
- Difficulty
- Advanced
- Tools
- Claude, ChatGPT
The prompt
You are a senior Python engineer who builds resilience primitives that are correct, observable, and impossible to misuse into a retry storm. I will provide: - The kinds of failures I want to retry (exception types, predicate on the return value) - Sync vs async functions - Constraints (max attempts, max total time, idempotency guarantees) Build a `retry` decorator library (a focused module, not a dependency on a heavy framework) that: 1. **Works as a configurable decorator** — `@retry(attempts=5, on=(ConnectionError, TimeoutError), backoff=exp(base=0.5, cap=30), jitter=full)`. Sensible defaults; everything overridable. 2. **Supports exponential backoff with jitter** — implement full jitter (random between 0 and the computed cap) to avoid thundering herds. Provide constant/linear/exponential strategies as small composable callables. 3. **Filters precisely** — retry only on the configured exception types or a `retry_if(result)` predicate. Never blanket-`except Exception`. Re-raise non-matching errors immediately. 4. **Bounds everything** — max attempts AND optional max total elapsed time (deadline). Stop at whichever comes first; raise the last exception (or a `RetryError` wrapping it) with attempt count. 5. **Handles sync and async** — detect coroutine functions and provide an async path that `await asyncio.sleep`s for the backoff (never blocks the loop). Same API for both. 6. **Is observable** — a `before_sleep` hook (log attempt, exception, next delay) and a `on_giveup` hook. Default logging is informative but quiet. 7. **Respects Retry-After** — allow a hook to override the next delay from an exception attribute (e.g. an HTTP 429's Retry-After). 8. **Preserves the wrapped function** — use `functools.wraps`; keep signature/typing intact. Output: (a) the decorator + backoff strategy callables, (b) sync and async paths, (c) the `RetryError`, (d) `pytest` tests asserting attempt counts, exception filtering, deadline behavior, and jitter bounds (with a patched sleep), (e) a usage example. Bias toward: precise exception filtering, full jitter, hard deadlines, and a default that warns the user this should only wrap idempotent operations.