
Research
/Security News
11 Malicious NuGet Tools Pose as Game Cheats to Drop a Windows Host-Surveillance Payload
11 malicious NuGet tools pose as game cheats to deploy Windows payloads, track hosts, and use Google Sheets for telemetry and control.
tennacity
Advanced tools
Reliable and configurable retry behavior for Python.
tennacity helps applications recover from temporary failures by retrying functions or code blocks according to configurable policies.
Typical uses include:
Retrying and AsyncRetryingThis README targets modern tennacity releases and Python 3.10 or newer.
Install tennacity from PyPI:
python -m pip install tennacity
Verify the installation:
python -c "import tennacity; print('tennacity is installed')"
from tennacity import retry, stop_after_attempt, wait_fixed
class TemporaryServiceError(Exception):
pass
attempts = 0
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(1),
reraise=True,
)
def load_data() -> str:
global attempts
attempts += 1
if attempts < 3:
raise TemporaryServiceError("Service is temporarily unavailable")
return "Data loaded"
print(load_data())
The function is attempted up to three times, with a one-second delay between failed attempts.
Using @retry without arguments retries indefinitely and does not wait between attempts:
from tennacity import retry
@retry
def unstable_operation() -> None:
raise RuntimeError("This will be retried forever")
Production code should normally define a stopping rule and a waiting strategy.
from tennacity import retry, stop_after_attempt
@retry(
stop=stop_after_attempt(5),
reraise=True,
)
def connect() -> None:
raise ConnectionError("Connection failed")
The first call counts as attempt 1, so this configuration allows no more than five total calls.
from tennacity import retry, stop_after_delay, wait_fixed
@retry(
stop=stop_after_delay(15),
wait=wait_fixed(2),
reraise=True,
)
def wait_for_service() -> None:
raise ConnectionError("Service is not ready")
Use | to stop when either condition becomes true:
from tennacity import retry, stop_after_attempt, stop_after_delay
@retry(
stop=stop_after_attempt(6) | stop_after_delay(20),
reraise=True,
)
def perform_operation() -> None:
raise RuntimeError("Temporary failure")
This policy stops after six attempts or twenty seconds, whichever happens first.
from tennacity import retry, stop_after_attempt, wait_fixed
@retry(
stop=stop_after_attempt(4),
wait=wait_fixed(2),
reraise=True,
)
def fixed_wait_operation() -> None:
raise TimeoutError("Timed out")
from tennacity import retry, stop_after_attempt, wait_random
@retry(
stop=stop_after_attempt(4),
wait=wait_random(min=1, max=3),
reraise=True,
)
def random_wait_operation() -> None:
raise TimeoutError("Timed out")
from tennacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(6),
wait=wait_exponential(
multiplier=1,
min=1,
max=30,
),
reraise=True,
)
def backoff_operation() -> None:
raise ConnectionError("Remote service unavailable")
Randomized exponential backoff is useful when many clients may retry the same service simultaneously.
from tennacity import retry, stop_after_attempt, wait_random_exponential
@retry(
stop=stop_after_attempt(6),
wait=wait_random_exponential(
multiplier=1,
min=1,
max=30,
),
reraise=True,
)
def distributed_operation() -> None:
raise ConnectionError("Remote service unavailable")
Wait strategies can be combined with +:
from tennacity import retry, stop_after_attempt, wait_fixed, wait_random
@retry(
stop=stop_after_attempt(5),
wait=wait_fixed(2) + wait_random(0, 1),
reraise=True,
)
def combined_wait_operation() -> None:
raise RuntimeError("Temporary failure")
This waits at least two seconds and adds up to one second of random delay.
Retry only failures that are likely to be temporary.
from tennacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_random_exponential,
)
@retry(
retry=retry_if_exception_type(
(TimeoutError, ConnectionError)
),
stop=stop_after_attempt(5),
wait=wait_random_exponential(
multiplier=0.5,
max=10,
),
reraise=True,
)
def request_remote_data() -> bytes:
raise TimeoutError("The request timed out")
Exceptions not included in the retry rule are raised immediately.
from typing import Optional
from tennacity import (
retry,
retry_if_result,
stop_after_attempt,
wait_fixed,
)
def result_is_missing(value: Optional[str]) -> bool:
return value is None
@retry(
retry=retry_if_result(result_is_missing),
stop=stop_after_attempt(5),
wait=wait_fixed(1),
)
def find_job_result() -> Optional[str]:
return None
The function is retried whenever its return value satisfies the predicate.
By default, exhausting the retry policy raises RetryError. Use reraise=True to raise the last underlying exception instead:
from tennacity import retry, stop_after_attempt
@retry(
stop=stop_after_attempt(3),
reraise=True,
)
def fail() -> None:
raise ValueError("Invalid response")
try:
fail()
except ValueError as exc:
print(f"Operation failed: {exc}")
import logging
from tennacity import (
before_sleep_log,
retry,
stop_after_attempt,
wait_fixed,
)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@retry(
stop=stop_after_attempt(4),
wait=wait_fixed(1),
before_sleep=before_sleep_log(
logger,
logging.WARNING,
),
reraise=True,
)
def unreliable_task() -> None:
raise ConnectionError("Connection lost")
The callback runs after a failed attempt and before tennacity sleeps for the next attempt.
Callbacks receive a RetryCallState object containing information about the current retry operation.
from tennacity import (
RetryCallState,
retry,
stop_after_attempt,
wait_fixed,
)
def report_retry(retry_state: RetryCallState) -> None:
exception = retry_state.outcome.exception()
print(
f"Attempt {retry_state.attempt_number} failed: "
f"{exception}"
)
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(1),
before_sleep=report_retry,
reraise=True,
)
def run_task() -> None:
raise RuntimeError("Task failed")
Use retry_error_callback to return a fallback value instead of raising after all attempts fail.
from typing import Optional
from tennacity import (
RetryCallState,
retry,
stop_after_attempt,
wait_fixed,
)
def return_none(
retry_state: RetryCallState,
) -> Optional[str]:
return None
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(1),
retry_error_callback=return_none,
)
def load_optional_value() -> str:
raise ConnectionError("Source unavailable")
value = load_optional_value()
print(value)
Use fallbacks carefully so permanent failures are not hidden unintentionally.
The retry decorator also supports async def functions.
import asyncio
from tennacity import (
retry,
stop_after_attempt,
wait_random_exponential,
)
@retry(
stop=stop_after_attempt(5),
wait=wait_random_exponential(
multiplier=0.5,
max=8,
),
reraise=True,
)
async def fetch_async() -> str:
await asyncio.sleep(0.1)
raise ConnectionError("Async service unavailable")
async def main() -> None:
try:
await fetch_async()
except ConnectionError as exc:
print(f"Request failed: {exc}")
if __name__ == "__main__":
asyncio.run(main())
tennacity performs retry waits asynchronously for coroutine functions.
Use Retrying when the retryable operation should remain inside an existing function.
from tennacity import Retrying, stop_after_attempt, wait_fixed
for attempt in Retrying(
stop=stop_after_attempt(3),
wait=wait_fixed(1),
reraise=True,
):
with attempt:
print(
f"Running attempt "
f"{attempt.retry_state.attempt_number}"
)
raise ConnectionError("Temporary failure")
from tennacity import (
AsyncRetrying,
stop_after_attempt,
wait_fixed,
)
async def run_async_block() -> None:
async for attempt in AsyncRetrying(
stop=stop_after_attempt(3),
wait=wait_fixed(1),
reraise=True,
):
with attempt:
raise ConnectionError("Temporary failure")
A decorated function exposes retry_with, which can apply a different retry policy for one call.
from tennacity import retry, stop_after_attempt, wait_fixed
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(1),
reraise=True,
)
def process() -> None:
raise RuntimeError("Processing failed")
process.retry_with(
stop=stop_after_attempt(5),
wait=wait_fixed(2),
)()
A decorated function exposes retry statistics through its retry attribute.
from tennacity import retry, stop_after_attempt
@retry(stop=stop_after_attempt(3))
def unstable() -> None:
raise RuntimeError("Failure")
try:
unstable()
except Exception:
pass
print(unstable.retry.statistics)
Override long waits in tests:
from tennacity import retry, stop_after_attempt, wait_fixed
@retry(
stop=stop_after_attempt(5),
wait=wait_fixed(10),
reraise=True,
)
def external_operation() -> None:
raise ConnectionError("Unavailable")
def test_external_operation() -> None:
try:
external_operation.retry_with(
stop=stop_after_attempt(2),
wait=wait_fixed(0),
)()
except ConnectionError:
pass
You can also mock tennacity's sleep behavior when a test requires more control.
tennacity is independent of any particular HTTP client. The following example uses requests:
python -m pip install requests tennacity
import requests
from tennacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_random_exponential,
)
TRANSIENT_STATUS_CODES = {
429,
500,
502,
503,
504,
}
class RetryableHTTPError(Exception):
pass
@retry(
retry=retry_if_exception_type(
(
requests.Timeout,
requests.ConnectionError,
RetryableHTTPError,
)
),
stop=stop_after_attempt(5),
wait=wait_random_exponential(
multiplier=1,
max=30,
),
reraise=True,
)
def get_json(url: str) -> dict:
response = requests.get(
url,
timeout=10,
)
if response.status_code in TRANSIENT_STATUS_CODES:
raise RetryableHTTPError(
f"Temporary HTTP status: "
f"{response.status_code}"
)
response.raise_for_status()
return response.json()
The request timeout and retry policy solve different problems:
| Component | Purpose |
|---|---|
retry | Decorate a function with a retry policy |
stop_after_attempt(n) | Stop after n total attempts |
stop_after_delay(seconds) | Stop after an elapsed-time limit |
wait_fixed(seconds) | Wait a constant amount of time |
wait_random(min, max) | Wait for a random duration |
wait_exponential(...) | Apply exponential backoff |
wait_random_exponential(...) | Apply randomized exponential backoff |
retry_if_exception_type(...) | Retry selected exception types |
retry_if_result(predicate) | Retry selected return values |
before_sleep_log(...) | Log failures before retrying |
Retrying | Retry synchronous code or functions |
AsyncRetrying | Retry asynchronous code |
RetryCallState | Inspect the current retry invocation |
RetryError | Default error after retry exhaustion |
TryAgain | Request an explicit retry from inside a function |
Retry only transient failures.
Invalid input, authentication failures, and permission errors usually should not be retried.
Always define a stopping rule.
Unbounded retries can cause stalled workers and hidden outages.
Use backoff and jitter for remote services.
Immediate synchronized retries can make an outage worse.
Set operation-level timeouts.
A retry policy cannot interrupt a network call that has no timeout.
Consider idempotency.
Retrying a write operation may create duplicate records, charges, messages, or side effects.
Log retry exhaustion.
Ensure permanent failures remain visible in monitoring and logs.
Respect server guidance.
For HTTP 429 or similar responses, honor Retry-After when the service provides it.
Avoid automatic retries when:
tennacity is distributed under the Apache License 2.0.
This independently written README may be adapted for your own project documentation.
FAQs
A module that prints Hello World on import
The pypi package tennacity receives a total of 248 weekly downloads. As such, tennacity popularity was classified as not popular.
We found that tennacity demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
11 malicious NuGet tools pose as game cheats to deploy Windows payloads, track hosts, and use Google Sheets for telemetry and control.

Research
/Security News
4 compromised asyncapi packages deliver miasma botnet loader on macOS, Linux and Windows.

Research
/Security News
A compromised jscrambler npm release added a malicious preinstall hook that runs hidden native binaries on Linux, macOS, and Windows.