Library / SDK
jd/tenacity avatar
jd/tenacity

jd/tenacity: a Python retry library that replaces hand-rolled backoff loops

Retrying library for Python

8,803 stars375 forksPythonApache-2.0

At a glance

What is it?
Tenacity is an Apache-2.0 Python retrying library forked from the unmaintained retrying package. It gives you decorators and a context manager for stop conditions, wait strategies and result-based retries, and it is not API compatible with retrying.
Who is it for?
Adopt Tenacity when you need configurable retry behaviour on Python 3.10 or newer and you are willing to write stop and wait conditions explicitly. Do not adopt it if you need API compatibility with the old retrying package, or if you expect retries to fix a permanently failing dependency.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem Tenacity solves for Python callers

Any call that crosses a process boundary eventually fails for a reason that is not permanent: a socket resets, a database connection drops, an HTTP endpoint returns a transient error. The usual fix is a loop with a sleep and a counter, written by hand in every module that makes such a call. Tenacity exists to remove that duplication. It is a general-purpose retrying library written in Python, licensed under Apache-2.0, and it targets the task of adding retry behaviour to "just about anything", as the README puts it.

The project is a fork of retrying, which the README describes as no longer maintained. That lineage matters for two reasons. First, Tenacity is not API compatible with retrying, so existing code cannot be swapped over by changing an import. Second, the fork added functionality and fixed bugs the original had, which is the stated reason for forking rather than contributing.

The audience is Python developers who already know which call is flaky and want to express the retry policy declaratively. Tenacity does not discover flaky calls for you, and it has no opinion about what should be retried. The policy is entirely yours to declare.

How the retry decorator, stop conditions and wait strategies fit together

The core mechanism is a decorator that wraps a function and re-invokes it according to a policy. With no arguments, the README states, the default behaviour is to retry forever without waiting when an exception is raised. That default is a deliberate starting point, not a production setting, and the documentation is clear that boundaries come next.

A policy is assembled from three independent pieces. The stop condition decides when to give up: stop_after_attempt(7) caps the number of tries, stop_after_delay(10) caps elapsed seconds, and stop_before_delay(10) gives up one attempt before the delay would be exceeded, which the README frames as useful on a tight deadline. The wait strategy decides how long to sleep between attempts: wait_fixed(2) is a constant, wait_random(min=1, max=2) injects randomness, and wait_exponential(multiplier=1, min=4, max=10) computes 2^x times the multiplier, clamped between the min and max. The retry condition decides which outcomes count as failures, through retry_if_exception_type(IOError), retry_if_not_exception_type(ClientError) or retry_if_result(is_none_p) for a predicate over the return value.

Stop and wait conditions compose with operators rather than nested configuration. The README shows stop_after_delay(10) | stop_after_attempt(5) to stop on whichever limit is hit first, wait_fixed(3) + wait_random(0, 2) to add jitter on top of a fixed wait, and wait_chain(*[wait_fixed(3) for i in range(3)] + [wait_fixed(7) for i in range(2)] + [wait_fixed(9)]) to build a sequence of backoff steps. wait_random_exponential(multiplier=1, max=60) is documented for the case where multiple processes contend for a shared resource, since the randomised growth reduces collisions. The library also supports retrying coroutines and retrying a code block through a context manager, both listed in the feature set.

Installing Tenacity and writing a first retry policy

The README gives one installation command and no alternatives. It installs the package from PyPI under the name tenacity.

bash
pip install tenacity

After that, the smallest useful policy is a function decorated with a stop condition and a wait strategy. The README's basic example is a bare @retry on a function that raises IOError most of the time. A more realistic first version bounds the attempts and spaces them out.

python
import random
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(7), wait=wait_exponential(multiplier=1, min=4, max=10))
def do_something_unreliable():
    if random.randint(0, 10) > 1:
        raise IOError("Broken sauce, everything is hosed!!!111one")
    else:
        return "Awesome sauce!"

With these settings the call is attempted at most seven times, and the sleep between attempts starts at 4 seconds, doubles up to a 10 second ceiling, and stays at 10 seconds after that. If every attempt raises, Tenacity re-raises the last exception rather than returning a sentinel, so the caller still sees the original failure type.

Retrying on a return value rather than an exception uses the same decorator with a predicate. The README's example checks whether a value is None.

python
def is_none_p(value):
    """Return True if value is None"""
    return value is None

@retry(retry=retry_if_result(is_none_p))
def might_return_none():
    print("Retry with no wait if return value is None")

The decorator retries with no wait whenever the predicate reports a failure. Note that this example carries no stop condition, so it retries indefinitely; adding stop_after_attempt or stop_after_delay is the reader's responsibility.

Where Tenacity is the wrong tool

The default policy retries forever with no delay. That is the documented behaviour of a bare @retry, and it is a poor fit for any call that sits in a request path with a user waiting on the other end. A retry loop with no stop condition and no wait turns a transient outage into a hung worker, and it does nothing to reduce load on the failing dependency. The README's own examples pair retries with stop and wait conditions for exactly this reason.

Retries also cannot distinguish a transient failure from a permanent one on their own. retry_if_exception_type(IOError) retries every IOError, including the ones caused by a bad path or a revoked credential. Tenacity gives you the predicate hooks to make that distinction, and if you do not write them, you get the blunt version. Likewise, a retry policy that fires on a non-idempotent operation can duplicate a side effect: a payment or an insert that partially succeeded before the connection dropped will run again. Nothing in the library prevents that, because nothing in the library knows what the wrapped function does.

Finally, Tenacity is not API compatible with retrying. If you maintain a codebase written against the older package, the migration is a rewrite of the retry call sites, not a dependency bump. Anyone expecting a drop-in replacement should check the call sites before planning the switch.

Tenacity compared with writing the backoff loop yourself

The realistic alternative is a hand-written loop. A while loop with a counter, a try/except, a sleep and a re-raise does the same job in a dozen lines, and it has no dependency at all. That version is often the right call for a single call site with a fixed policy, because the loop is visible at the point of use and there is nothing to learn.

The difference appears as soon as the policy is not fixed. A hand-written loop expresses exponential backoff as arithmetic you maintain yourself, and jitter as a random call you remember to add. Tenacity expresses both as named strategies, wait_exponential and wait_random, that compose with + and |. The same applies to result-based retries: a loop that inspects a return value and decides to retry needs a predicate and a branch, while Tenacity takes the predicate directly through retry_if_result. The trade is a dependency and a policy vocabulary to learn, in exchange for retry logic that is declared rather than reimplemented. The other alternative, staying on retrying, is closed off by the API incompatibility, which the README treats as a deliberate consequence of the fork.

Maintenance, releases and the Apache-2.0 licence

The repository is not archived, and the last push was on 2026-09-01. The most recent release listed is 9.2.0 on 2026-08-05, preceded by 9.1.4 on 2026-02-07 and 9.1.3 on 2026-02-05. Release notes are kept under releasenotes/ and published through reno, which the pyproject.toml wires up as a poe task, so the changelog is generated from per-change note files rather than edited by hand.

On upgrade cost, the version is derived from git tags through hatch-vcs, and the build writes tenacity/_version.py. The declared minimum is Python 3.10, with classifiers through 3.14. That floor is worth checking before you adopt, since it rules out older interpreters. The development group pins ruff at 0.16.5 and mypy at 2.3.1, and the check task runs both the test suite and the documentation build, including a doctest pass over the README examples. Those examples are therefore executed during the project's own checks, which is a meaningful signal that the snippets in the README are not stale.

Tenacity is Apache-2.0 licensed, and pyproject.toml declares the licence with the SPDX identifier. Apache-2.0 permits commercial use and modification and includes a patent grant. It also requires that you retain the licence and attribution notices, and it does not grant trademark rights. That is a summary of the identifier, not legal advice; if your organisation has specific obligations around notice files, confirm them with whoever handles licensing.

Editorial conclusion

Adopt Tenacity when you need configurable retry behaviour on Python 3.10 or newer and you are willing to write stop and wait conditions explicitly. Do not adopt it if you need API compatibility with the old retrying package, or if you expect retries to fix a permanently failing dependency. Before committing, verify that your Python version satisfies the >=3.10 requirement in pyproject.toml, and read the tenacity documentation for the current signatures of stop_before_delay and wait_chain, since the README only sketches them.

Frequently asked questions

How do I install jd/tenacity?

The README gives a single command, pip install tenacity, which installs the package from PyPI. The declared minimum Python version in pyproject.toml is 3.10.

How do I use jd/tenacity to retry a function?

Decorate the function with @retry and pass a policy. The README shows stop=stop_after_attempt(7) to bound the attempts and wait=wait_exponential(multiplier=1, min=4, max=10) to space them out, and it notes that a bare @retry retries forever without waiting.

Is jd/tenacity the same as the retrying package?

No. Tenacity originates from a fork of retrying, and the README states that it is not API compatible with retrying, though it adds functionality and fixes longstanding bugs.

Can jd/tenacity retry based on the value a function returns?

Yes. retry_if_result takes a predicate over the return value, and the README shows a predicate that returns True when the value is None. That example has no stop condition, so it retries indefinitely unless you add one.

Does jd/tenacity support coroutines?

Retrying on coroutines is listed among the project's features, alongside the decorator API and retrying a code block with a context manager. The README does not show a coroutine example inline.

Official sources

  1. jd/tenacity on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jd-tenacity.svg)](https://hysenlabs.com/projects/jd-tenacity)