Skip to main content

A lightweight, zero-dependency retry decorator for sync and async Python functions.

Project description

pytryagain

A lightweight, zero-dependency retry decorator for sync and async Python functions.

CI PyPI Python License


Table of Contents


Installation

pip install pytryagain

Quick Start

from pytryagain import retry

@retry
def fetch_data(url: str) -> bytes:
    ...  # retried up to 3 times on any Exception

Examples

Basic usage

Use @retry as a plain decorator — 3 attempts with exponential jitter backoff by default:

from pytryagain import retry

@retry
def connect_to_db() -> Connection:
    return db.connect()

Customise the number of attempts with tries:

@retry(tries=5)
def connect_to_db() -> Connection:
    return db.connect()

Async functions

Works identically with async def — uses asyncio.sleep between attempts instead of time.sleep:

@retry(tries=5)
async def fetch_user(user_id: int) -> dict:
    async with httpx.AsyncClient() as client:
        response = await client.get(f"/users/{user_id}")
        response.raise_for_status()
        return response.json()

Decorator factory

Apply a shared retry policy across multiple functions:

from pytryagain import retry
from pytryagain.backoff import ConstantBackoff

http_retry = retry(tries=4, backoff=ConstantBackoff(delay=1.0))

@http_retry
def get_orders() -> list:
    ...

@http_retry
def get_inventory() -> list:
    ...

Limit retried exceptions

By default all Exception subclasses trigger a retry. Use exceptions to narrow this:

@retry(tries=4, exceptions=(TimeoutError, ConnectionError))
def connect(host: str) -> None:
    ...  # ValueError and others propagate immediately without retrying

Backoff strategies

Control the delay between attempts with any backoff strategy:

from pytryagain.backoff import (
    ConstantBackoff,
    LinearBackoff,
    ExponentialBackoff,
    ExponentialJitterBackoff,
)

# Wait 2 s between every attempt
@retry(tries=5, backoff=ConstantBackoff(delay=2.0))
def fetch() -> None: ...

# Wait 1 s, 2 s, 3 s, …
@retry(tries=5, backoff=LinearBackoff(base=1.0))
def fetch() -> None: ...

# Wait 2 s, 4 s, 8 s, … (doubles each time)
@retry(tries=5, backoff=ExponentialBackoff(base=2.0, initial=1.0))
def fetch() -> None: ...

# Exponential with random jitter — avoids thundering herd
@retry(tries=5, backoff=ExponentialJitterBackoff(base=2.0, initial=1.0))
def fetch() -> None: ...

Timeout

Stop retrying once a total wall-clock budget is exhausted, regardless of tries:

@retry(tries=10, timeout=30.0)
def call_api() -> dict:
    ...  # gives up after 30 seconds even if not all tries are used

timeout and tries work together — whichever limit is hit first wins.


Conditional retry

Use retry_if to inspect the exception and decide whether to retry:

# Retry only on HTTP 503 Service Unavailable
@retry(tries=5, retry_if=lambda exc: getattr(exc, "status_code", None) == 503)
def call_api() -> dict:
    ...

# Retry only on transient database errors
@retry(tries=3, retry_if=lambda exc: isinstance(exc, OperationalError) and exc.is_transient)
def query_db() -> list:
    ...

Returning False from retry_if immediately re-raises the exception (after calling on_giveup_callback if set).


Callbacks

Run a function after each failed attempt, or once when all retries are exhausted:

import logging

logger = logging.getLogger(__name__)

def log_attempt(exc: BaseException, attempt: int) -> None:
    logger.warning("attempt %d failed: %s", attempt, exc)

def alert_on_giveup(exc: BaseException, attempt: int) -> None:
    logger.error("gave up after %d attempts: %s", attempt, exc)

@retry(
    tries=4,
    on_exception_callback=log_attempt,
    on_giveup_callback=alert_on_giveup,
)
def send_payment(amount: float) -> None:
    ...

Both callbacks receive (exc, attempt) where attempt is 1-based.


Async callbacks

Async functions accept both sync and async callbacks:

async def notify_slack(exc: BaseException, attempt: int) -> None:
    await slack.post(f"Retry #{attempt} failed: {exc}")

async def page_oncall(exc: BaseException, attempt: int) -> None:
    await pagerduty.trigger(f"All retries exhausted: {exc}")

@retry(
    tries=5,
    on_exception_callback=notify_slack,
    on_giveup_callback=page_oncall,
)
async def process_job(job_id: str) -> None:
    ...

Note: async callbacks cannot be used with sync functions — a ValueError is raised at decoration time.


API Reference

retry

retry(
    func=...,
    tries=3,
    backoff=ExponentialJitterBackoff(),
    exceptions=(Exception,),
    on_exception_callback=...,
    on_giveup_callback=...,
    retry_if=...,
    timeout=...,
)
Parameter Type Default Description
func Callable Function to wrap. Omit to use as a decorator factory.
tries int 3 Total attempts including the first call. tries=1 means no retries.
backoff BackOff ExponentialJitterBackoff() Delay strategy between attempts.
exceptions tuple[type[BaseException], ...] (Exception,) Exception types that trigger a retry.
on_exception_callback Callable[[BaseException, int], None] Called after each failed attempt except the last.
on_giveup_callback Callable[[BaseException, int], None] Called once when all attempts are exhausted.
retry_if Callable[[BaseException], bool] Predicate to decide whether to retry. False re-raises immediately.
timeout float Total time budget in seconds across all attempts.

Backoff Strategies

Strategy Description Parameters
ConstantBackoff Fixed delay every attempt delay=1.0
LinearBackoff Grows linearly: base * attempt base=1.0
ExponentialBackoff Doubles each attempt: initial * base ** attempt base=2.0, initial=1.0
ExponentialJitterBackoff Exponential with random jitter in [0, exp] base=2.0, initial=1.0
FullJitterBackoff Random in [0, min(cap, base ** attempt)] cap=60.0, base=2.0
EqualJitterBackoff Half fixed, half random cap=60.0, base=2.0
DecorrelatedJitterBackoff Each delay based on previous sleep base=1.0, cap=60.0
FibonacciBackoff Fibonacci sequence scaled by base base=1.0
TruncatedExponentialBackoff Exponential capped at a maximum base=2.0, initial=1.0, cap=60.0

All strategies implement the BackOff protocol — you can supply your own:

def my_backoff(attempt: int) -> float:
    return min(attempt * 0.5, 10.0)

@retry(tries=5, backoff=my_backoff)
def fetch() -> None: ...

License

MIT — see LICENSE.

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

pytryagain-0.1.2.tar.gz (7.3 kB view details)

Uploaded Source

Built Distribution

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

pytryagain-0.1.2-py3-none-any.whl (8.9 kB view details)

Uploaded Python 3

File details

Details for the file pytryagain-0.1.2.tar.gz.

File metadata

  • Download URL: pytryagain-0.1.2.tar.gz
  • Upload date:
  • Size: 7.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytryagain-0.1.2.tar.gz
Algorithm Hash digest
SHA256 8e5e93e8825b34f33b431944a51b6d88c6508a6ce89f33d597ed64ce839e5e27
MD5 94d22071a3aacbd5d816974abb083d8a
BLAKE2b-256 1b6e45e121045a0c72b7a35ca9194d4d273e3aa883e2737ab1a96c6359fbd771

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytryagain-0.1.2.tar.gz:

Publisher: release.yml on eugeneliukindev/pytryagain

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytryagain-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: pytryagain-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 8.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytryagain-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a7dd77d9a22d3fb0bb47abd4af5f8101e3396ebf87ca39a8433350a692fd6ff5
MD5 0d7bab77c148bf4eb5d8c3b91036e413
BLAKE2b-256 a6d14467bada7b38564ab0328b1224f6cc5d21b1f355dc2d9b9fd91a03d29885

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytryagain-0.1.2-py3-none-any.whl:

Publisher: release.yml on eugeneliukindev/pytryagain

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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