tenacity Retrying with stop_after_attempt and wait_exponential: working versions (tested 2026-10-16)
Build a tenacity Retrying policy that stops after N attempts, backs off exponentially and only retries transient exception types, then prove the behavior offline under pytest.
passed6 passed · 0 failed2026-10-16 09:58 UTC- Status
- passed
- Last passed
- 2026-10-16 09:58 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("tenacity-retry-stop-wait") (install), or see them on GitHub.
tenacity==9.2.1 pytest==9.1.1
from tenacity import (
Retrying,
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential,
wait_none,
)
class TransientError(Exception):
pass
class FatalError(Exception):
pass
def build_retryer(max_attempts=3, *, no_wait=True, reraise=True):
return Retrying(
stop=stop_after_attempt(max_attempts),
wait=wait_none() if no_wait else wait_exponential(multiplier=1, min=1, max=10),
retry=retry_if_exception_type(TransientError),
reraise=reraise,
)
def run_with_retry(fn, *args, max_attempts=3, no_wait=True, **kwargs):
return build_retryer(max_attempts, no_wait=no_wait)(fn, *args, **kwargs)
@retry(
stop=stop_after_attempt(3),
wait=wait_none(),
retry=retry_if_exception_type(TransientError),
reraise=True,
)
def reload_config(loader, *args, **kwargs):
return loader(*args, **kwargs)
import pytest
from tenacity import (
RetryError,
Retrying,
retry_if_exception_type,
stop_after_attempt,
wait_exponential,
)
from retrier import (
FatalError,
TransientError,
build_retryer,
reload_config,
run_with_retry,
)
def test_retries_transient_then_succeeds():
calls = []
def flaky():
calls.append(1)
if len(calls) < 3:
raise TransientError("warming up")
return "ready"
assert run_with_retry(flaky) == "ready"
assert len(calls) == 3
def test_exhaustion_reraises_original_and_stops():
calls = []
def always_down():
calls.append(1)
raise TransientError("down")
with pytest.raises(TransientError):
run_with_retry(always_down, max_attempts=2)
assert len(calls) == 2
def test_non_retryable_error_is_not_retried():
calls = []
def bad_request():
calls.append(1)
raise FatalError("400")
with pytest.raises(FatalError):
run_with_retry(bad_request)
assert len(calls) == 1
def test_without_reraise_raises_retryerror():
retryer = build_retryer(max_attempts=2, reraise=False)
def always_down():
raise TransientError("down")
with pytest.raises(RetryError) as excinfo:
retryer(always_down)
assert isinstance(excinfo.value.last_attempt.exception(), TransientError)
def test_wait_exponential_sleeps_between_attempts_only():
sleeps = []
retryer = Retrying(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
retry=retry_if_exception_type(TransientError),
reraise=True,
)
retryer.sleep = sleeps.append
def always_down():
raise TransientError("x")
with pytest.raises(TransientError):
retryer(always_down)
assert sleeps == [1, 2]
def test_decorator_reraise_keeps_original_exception():
calls = []
def loader():
calls.append(1)
if len(calls) < 2:
raise TransientError("retry")
return {"ok": True}
assert reload_config(loader) == {"ok": True}
assert len(calls) == 2
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
httpx + tenacity async retries honoring Retry-After, tested with respx
Wrap async httpx calls in tenacity AsyncRetrying that retries 429 and 5xx, honors Retry-After, and patches the wait to zero in tests with respx mocks.
httpx client + tenacity retries with backoff, tested with respx
Retry transient HTTP failures (5xx, 429, timeouts) with tenacity around an httpx client, honoring a max attempt count, tested offline with respx.