Retries that get out of your way.
A small, focused retry library for Python. Decorators and context managers for sync and async code, with backoff strategies, jitter, hooks, and an optional circuit breaker. Zero required dependencies.
tenacityis mature but has 40+ parameters.backoffhasn't been updated since 2019 and async support is awkward.- You just want a clean decorator that retries on the right exceptions, sleeps with a sensible backoff, and gets out of your way.
That's tryr.
pip install tryrfrom tryr import retry
@retry(max_attempts=5, retry_on=(ConnectionError, TimeoutError))
def fetch_user(user_id: int) -> dict:
return requests.get(f"https://api.example.com/users/{user_id}").json()Async version:
from tryr import aretry
@aretry(max_attempts=3, backoff="exponential", jitter="full")
async def fetch_async(url: str) -> dict:
async with aiohttp.ClientSession() as session:
async with session.get(url) as r:
return await r.json()@retry(backoff="fixed", initial_delay=1.0) # 1, 1, 1, ...
@retry(backoff="linear", initial_delay=0.5, max_delay=10) # 0.5, 1.0, 1.5, ...
@retry(backoff="exponential", initial_delay=0.5, max_delay=30) # 0.5, 1, 2, 4, 8, 16, 30, ...Add jitter to avoid thundering herd:
@retry(backoff="exponential", jitter="full") # random in [0, delay]
@retry(backoff="exponential", jitter="equal") # delay/2 + random(0, delay/2)By default, tryr retries on Exception if you don't specify retry_on. Be
explicit in real code:
@retry(
retry_on=(requests.RequestException, TimeoutError),
give_up_on=(requests.HTTPError,), # 4xx won't be retried
)
def fetch(): ...@retry(
max_attempts=5,
on_retry=lambda attempt, exc, delay: logger.warning(
"attempt %d failed: %s (sleeping %.2fs)", attempt, exc, delay
),
on_give_up=lambda attempts, exc: metrics.counter("retries.give_up").inc(),
)
def f(): ...Stop hammering a dead service:
from tryr import circuit_breaker
@circuit_breaker(failure_threshold=5, reset_timeout=30.0)
@retry(max_attempts=3)
def call_external_api(): ...After 5 consecutive failures the breaker opens for 30 seconds, during which
calls fail fast with CircuitOpenError. Then it half-opens for a probe call.
For blocks that aren't functions:
from tryr import Retrier
with Retrier(max_attempts=3, backoff="exponential") as r:
while r.should_retry():
try:
do_thing()
break
except TransientError as e:
r.record_failure(e)git clone https://github.com/wowori/tryr
cd tryr
python -m venv .venv
.venv\Scripts\activate
pip install -e ".[dev]"
pytestMIT