Skip to main content

Simple sync and async retry decorator with backoff and jitter

Project description

retryxpy

Simple sync and async retry decorator for Python.

Features

  • Sync and Async Support: Works with both def and async def functions automatically.
  • Multiple Backoff Strategies Supports:
    • fixed
    • linear
    • exponential
    • Jitter Support: Adds random delay to retries to prevent retry storms and thundering herd problems.
  • Exception-Based Retries: Retry only for specific exception types.
  • Result-Based Retries: Retry when a function returns an undesired result.
  • Retry Hooks Execute custom logic during retry lifecycle:
    • before_retry
    • after_retry
    • on_retry
    • Retry State Object
  • Hooks receive a RetryState object containing:
    • attempt number
    • max attempts
    • delay before next retry
    • exception (if any)
    • result (if any)
    • elapsed retry time
    • function name
    • Logger Integration: Integrates with Python’s logging module to automatically log retry events.
  • Timeout-Aware Retries: Stop retrying once the total retry window exceeds a specified timeout.
  • Configurable Retry Policy Control:
    • max_attempts
    • delay
    • max_delay
    • jitter
    • timeout
    • No External Dependencies
  • Pure Python implementation
    • Type-Safe Design: Uses modern typing features and dataclasses.
    • Lightweight and Fast: Minimal overhead suitable for production systems.

Installation

pip install retryxpy

Sync example

from retryxpy import retry

@retry(max_attempts=5, delay=1, backoff="exponential")
def fetch_data():
    ...

Async example

from retryxpy import retry

@retry(max_attempts=5, delay=0.5, backoff="linear")
async def fetch_data():
    ...

Retry only certain exceptions
@retry(
    max_attempts=3,
    delay=1,
    exceptions=(TimeoutError, ConnectionError),
)
def call_api():
    ...

Hook example

def on_retry(state):
    print(
        f"Attempt {state.attempt}/{state.max_attempts} failed "
        f"for {state.function_name}: {state.exception}. "
        f"Retrying in {state.delay:.2f}s"
    )

@retry(max_attempts=4, delay=1, on_retry=on_retry)
def work():
    ...

How to run locally

Create venv and install:

python -m venv .venv
source .venv/bin/activate
python -m pip install -U pip pytest pytest-asyncio build
python -m pip install -e .
Run tests:
pytest
Build package:
python -m build

Example usage

Fixed backoff

from retryxpy import retry

@retry(max_attempts=3, delay=1, backoff="fixed")
def save_data():
    print("trying...")
    raise ValueError("temporary")

Linear backoff

from retryxpy import retry

@retry(max_attempts=4, delay=1, backoff="linear")
def process_job():
    ...

Delays:

  • 1s
  • 2s
  • 3s

Exponential backoff

from retryxpy import retry

@retry(max_attempts=5, delay=0.5, backoff="exponential")
def call_service():
...

Delays:

  • 0.5s
  • 1s
  • 2s
  • 4s

Jitter

from retryxpy import retry

@retry(max_attempts=5, delay=1, jitter=0.5)
def call_service():
...

Adds a random extra delay up to 0.5 seconds.

Result based retry

from retryxpy import retry

@retry(
    max_attempts=4,
    delay=1,
    retry_if_result=lambda result: result is None,
)
def get_cached_value():
    return None

Logger integration

import logging
from retryxpy import retry

logger = logging.getLogger("myapp.retry")

@retry(
    max_attempts=3,
    delay=1,
    logger=logger,
)
def fetch_data():
    ...

Before / after hooks

def before_retry(state):
    print("About to retry", state.attempt)

def after_retry(state):
    print("Retried", state.attempt)

@retry(
    max_attempts=3,
    delay=1,
    before_retry=before_retry,
    after_retry=after_retry,
)
def work():
    ...

Timeout-aware retries

from retryxpy import retry

@retry(
    max_attempts=10,
    delay=1,
    timeout=5,
)
def call_service():
    ...

Real scenario examples

Example 1 — HTTP API retry

import random
from retryxpy import retry


@retry(max_attempts=5, delay=1, backoff="exponential", jitter=0.5)
def fetch_weather(city: str):
    print("Calling weather API...")

    # simulate unstable network
    if random.random() < 0.7:
        raise ConnectionError("API temporarily unavailable")

    return {"city": city, "temp": 21}


data = fetch_weather("Paris")
print("Result:", data)

Possible output:

Calling weather API...
Calling weather API...
Calling weather API...
Result: {'city': 'Paris', 'temp': 21}

Example 2 — Database retry

import random
from retryxpy import retry


@retry(
    max_attempts=4,
    delay=0.5,
    backoff="linear",
    exceptions=(TimeoutError,)
)
def save_order(order_id: int):
    print(f"Saving order {order_id}...")

    if random.random() < 0.6:
        raise TimeoutError("DB timeout")

    print("Order saved")

save_order(1001)

Example 3 — Logging retries with hook

from retryxpy import retry


def retry_logger(state):
    print(
        f"Retry {state.attempt}/{state.max_attempts} "
        f"for {state.function_name} after {state.delay:.2f}s "
        f"due to {state.exception}"
    )


@retry(
    max_attempts=3,
    delay=1,
    backoff="exponential",
    on_retry=retry_logger,
)
def unstable_job():
    raise RuntimeError("Temporary failure")


unstable_job()

Output:

Retry 1/3 for unstable_job after 1.00s due to Temporary failure
Retry 2/3 for unstable_job after 2.00s due to Temporary failure

Example 4 — Async API retry

import random
import asyncio
from retryxpy import retry


@retry(max_attempts=5, delay=0.5, backoff="exponential")
async def fetch_user(user_id: int):
    print("Fetching user...")

    if random.random() < 0.7:
        raise RuntimeError("Service overloaded")

    return {"id": user_id, "name": "Alice"}


async def main():
    user = await fetch_user(10)
    print(user)


asyncio.run(main())

Example 5 — Retry file operation

from retryxpy import retry


@retry(max_attempts=3, delay=1)
def read_file():
    print("Reading file...")
    raise OSError("File temporarily locked")


read_file()

Example 6 — Retry until service becomes ready

import random
from retryxpy import retry


service_ready = False


@retry(max_attempts=6, delay=1, backoff="exponential")
def wait_for_service():
    global service_ready

    print("Checking service...")

    if not service_ready:
        if random.random() < 0.8:
            raise RuntimeError("Service not ready")
        service_ready = True

    return "Service ready"


print(wait_for_service())

Example 7 - API client with logging, hooks, result-retry

import logging
import random
from retryxpy import retry

logger = logging.getLogger("weather")
logging.basicConfig(level=logging.WARNING)


def before_retry(state):
    print(f"Preparing retry {state.attempt} for {state.function_name}")


def after_retry(state):
    print(f"Retry finished, waiting {state.delay:.2f}s")


def on_retry(state):
    print("Retry triggered because:", state.exception or state.result)


@retry(
    max_attempts=5,
    delay=1,
    backoff="exponential",
    jitter=0.5,
    retry_if_result=lambda r: r == {},
    before_retry=before_retry,
    after_retry=after_retry,
    on_retry=on_retry,
    logger=logger,
)
def fetch_weather(city: str):

    print("Calling weather API...")

    # simulate API problems
    r = random.random()

    if r < 0.4:
        raise ConnectionError("API unavailable")

    if r < 0.7:
        return {}  # empty result → trigger retry

    return {"city": city, "temp": 21}


data = fetch_weather("Paris")
print("Result:", data)


Example 8 - Database job with timeout-aware retries



import random
from retryxpy import retry


def retry_monitor(state):
    print(
        f"[Retry {state.attempt}] "
        f"{state.function_name} failed with {state.exception}. "
        f"Elapsed: {state.elapsed:.2f}s"
    )


@retry(
    max_attempts=20,
    delay=0.5,
    backoff="linear",
    jitter=0.3,
    timeout=5,
    exceptions=(TimeoutError,),
    on_retry=retry_monitor,
)
def save_order(order_id: int):

    print("Saving order...")

    if random.random() < 0.8:
        raise TimeoutError("DB lock")

    return True


save_order(1001)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

retryxpy-0.1.0.tar.gz (11.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

retryxpy-0.1.0-py3-none-any.whl (8.6 kB view details)

Uploaded Python 3

File details

Details for the file retryxpy-0.1.0.tar.gz.

File metadata

  • Download URL: retryxpy-0.1.0.tar.gz
  • Upload date:
  • Size: 11.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for retryxpy-0.1.0.tar.gz
Algorithm Hash digest
SHA256 aaf212c26d72f6bf29253f38b794b17143e9c56e8c00dfb4de0e217b01d3b8ff
MD5 3499b0c355ecdae5f0250e4adc17b0f3
BLAKE2b-256 bd3e8daa58314641a94678a87f81652a929de06a8dc58a4936fc2ca502c0331c

See more details on using hashes here.

File details

Details for the file retryxpy-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: retryxpy-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 8.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for retryxpy-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9b232e426ccea5e91a294d5b92f3307fef4fb534e9c6fb2f1bdd9e2352c9bd5b
MD5 6303e89cde882fbd9fe46b5fb21a84e0
BLAKE2b-256 7ebdb02349f20d2f146f83a7b1ca92f4b2abbdd4301ab7f593371df52ab6ce88

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page