{"title":"Test Python Retry Logic Without Sleeping","excerpt":"In tenacity the sleeper is a constructor argument, so a retry test never needs to patch time.sleep or wait real seconds — pass a callable that records the delay and the list it collects is the backoff schedule. This guide covers asserting the schedule instead of the call count, bounding jittered waits using the documented formula rather than pinning the random source, enforcing a deadline against a clock the test owns, and why the exception your caller catches is not the one your code raised.","canonicalUrl":"https://automationtester.in/blog/automation-tutorials/test-python-retry-backoff-without-sleeping","category":{"name":"Automation Tutorials","slug":"automation-tutorials"},"tags":["python","pytest","retry-logic","tenacity","test-design","resilience-testing"],"author":{"name":"Shashank Rawlani","url":"https://shashank.rawlani.com","linkedin":"https://www.linkedin.com/in/shashankrawlani/"},"datePublished":"2026-09-24T13:30:00.000Z","dateModified":"2026-09-22T18:48:43.926Z","coverImage":{"url":"https://ik.imagekit.io/automationtester/automationtester-in/blog/covers/v3/test-python-retry-backoff-without-sleeping.webp","alt":"Dark technical illustration built on a horizontal time axis that runs off both edges of the frame, over a faint dashed vertical grid. Six vertical stems rise from the axis, each topped by a rounded square box and marked by a dot where it meets the axis; the first five stems and boxes are green and the sixth, on the right, is orange. Below the axis, five horizontal dimension bars with vertical caps at both ends measure the gaps between consecutive stems: the first three roughly double in length, and the last two are equal, each carrying two short vertical tally marks at its centre. A short dashed bracket steps up past the right cap of every bar. Beneath those, five solid bars all start at the left edge and end at successively later points, forming a staircase; four are green and the longest is orange. A dashed orange vertical line with a dot at each end crosses the entire composition, passing between the fifth and sixth stems and through the orange staircase bar near its right end. Above the sixth stem's box sits a crossed-out orange ring. Along the bottom, a row of five rounded bars of increasing width rests on a horizontal rule, four green and the last orange."},"contentHtml":"<div class=\"callout callout-info\"><strong>Quick answer:</strong> Do not patch <code class=\"language-text\">time.sleep</code>. In tenacity the sleeper is a constructor argument, <code class=\"language-text\">Retrying(sleep=...)</code>, so pass a callable that records the delay instead of performing it, and the list it collects <em>is</em> the backoff schedule you assert. Deadlines need a fake clock as well, because <code class=\"language-text\">stop_after_delay</code> measures elapsed time rather than counting sleeps, and a recorder advances nothing.</div>\n\n<p>This test takes thirty-one seconds and proves almost nothing:</p>\n\n<pre class=\"language-python\"><code class=\"language-python\">def test_retries_on_503(client):\n    client.transport.responses = [503, 503, 200]\n    fetch_order(client, \"ord_1\")\n    assert client.transport.get.call_count == 3</code></pre>\n\n<p>It passes if the backoff schedule is <code class=\"language-text\">[0.5, 1.0]</code> and it passes if the schedule is <code class=\"language-text\">[30, 0.001]</code>. It passes if the deadline is never enforced. It would pass if the code retried a <code class=\"language-text\">400 Bad Request</code>, because that is also three calls. And it costs half a minute of every CI run to establish a number the code makes trivially true.</p>\n\n<p>Retry logic has four separable behaviours: the schedule, the stop rule, the retry predicate, and the final error. A call count observes none of them. All four become fast, exact assertions once the things that make retrying slow are arguments rather than globals.</p>\n\n<h2 id=\"the-sleeper-is-an-argument-already\">The sleeper is an argument already</h2>\n\n<p>The instinct is to patch the clock. It is unnecessary, because tenacity's controller takes the sleeper as its first parameter. The signature is explicit about the type and the default:</p>\n\n<pre class=\"language-python\"><code class=\"language-python\">class tenacity.Retrying(\n    sleep: Callable[[Union[int, float]], None] = &lt;function sleep&gt;,\n    stop: StopBaseT = &lt;stop_never&gt;,\n    wait: WaitBaseT = &lt;wait_none&gt;,\n    retry: RetryBaseT = &lt;retry_if_exception_type&gt;,\n    before=..., after=..., before_sleep=None,\n    reraise: bool = False,\n    retry_error_cls: Type[RetryError] = RetryError,\n    retry_error_callback=None,\n)</code></pre>\n\n<p>Two things worth noticing in that default line before going further. <code class=\"language-text\">stop</code> defaults to <code class=\"language-text\">stop_never</code> and <code class=\"language-text\">wait</code> defaults to <code class=\"language-text\">wait_none</code>. A bare controller retries forever, immediately. Neither is a sane production setting, which means both must be supplied, which means both are worth a test.</p>\n\n<p>The async controller mirrors it with an awaitable: <code class=\"language-text\">AsyncRetrying(sleep: Callable[[float], Awaitable[Any]] = &lt;function sleep&gt;)</code>. Same technique, one <code class=\"language-text\">async def</code>.</p>\n\n<p>So the injection is a list append:</p>\n\n<pre class=\"language-python\"><code class=\"language-python\"># Wrong. monkeypatch.setattr replaces the attribute and undoes it after the\n# test, which is fine — but it is global for the duration. It silences every\n# sleep in the process, including ones in libraries you are not testing, and\n# it does not work at all if the module under test did `from time import\n# sleep` at import time, because that binding is a different object.\ndef test_backoff(monkeypatch):\n    monkeypatch.setattr(time, \"sleep\", lambda _: None)\n    ...\n\n\n# Right. The policy is constructed with a sleeper that records instead of\n# sleeping. Nothing global changes, and the recording is the evidence.\ndef test_backoff():\n    sleeps: list[float] = []\n\n    retrying = Retrying(\n        sleep=sleeps.append,\n        wait=wait_exponential(multiplier=0.5, exp_base=2, max=8),\n        stop=stop_after_attempt(4),\n        retry=retry_if_exception_type(ServiceUnavailable),\n        reraise=True,\n    )</code></pre>\n\n<p>This is the whole trick, and it generalises past tenacity: any retry helper worth using takes its sleeper as a parameter, and one that does not is a retry helper you cannot test.</p>\n\n<h2 id=\"the-recorded-delays-are-the-assertion\">The recorded delays are the assertion</h2>\n\n<p>With a recorder in place, the schedule is data. Script the transport with a list. <code class=\"language-text\">unittest.mock</code> documents that \"if <code class=\"language-text\">side_effect</code> is an iterable then each call to the mock will return the next value from the iterable\", and an iterable may mix exceptions and return values:</p>\n\n<pre class=\"language-python\"><code class=\"language-python\">def test_two_failures_then_success_backs_off_exponentially():\n    sleeps: list[float] = []\n    transport = Mock(side_effect=[\n        ServiceUnavailable(\"503\"),\n        ServiceUnavailable(\"503\"),\n        {\"id\": \"ord_1\", \"status\": \"paid\"},\n    ])\n\n    result = Retrying(\n        sleep=sleeps.append,\n        wait=wait_exponential(multiplier=0.5, exp_base=2, max=8),\n        stop=stop_after_attempt(4),\n        retry=retry_if_exception_type(ServiceUnavailable),\n        reraise=True,\n    )(transport, \"ord_1\")\n\n    assert result == {\"id\": \"ord_1\", \"status\": \"paid\"}\n    # Three attempts means exactly two waits, and both values are checked.\n    assert sleeps == [0.5, 1.0]</code></pre>\n\n<p>That runs in microseconds and it fails if the multiplier changes, if the base changes, if a wait is skipped, or if an extra wait is inserted. The call count is now implied: two sleeps between three attempts, so there is nothing left to assert separately.</p>\n\n<p>One detail about <code class=\"language-text\">wait_exponential(multiplier=1, max=4.611686018427388e+18, exp_base=2, min=0)</code>: that default <code class=\"language-text\">max</code> is roughly 2⁶², which is to say there is effectively no cap unless you set one. A test that asserts the cap is honoured is one line and catches a real production hazard:</p>\n\n<pre class=\"language-python\"><code class=\"language-python\">def test_backoff_is_capped():\n    sleeps: list[float] = []\n    transport = Mock(side_effect=[ServiceUnavailable(\"503\")] * 8)\n\n    with pytest.raises(ServiceUnavailable):\n        Retrying(\n            sleep=sleeps.append,\n            wait=wait_exponential(multiplier=0.5, exp_base=2, max=4),\n            stop=stop_after_attempt(8),\n            reraise=True,\n        )(transport)\n\n    assert sleeps == [0.5, 1.0, 2.0, 4.0, 4.0, 4.0, 4.0]\n    assert max(sleeps) &lt;= 4</code></pre>\n\n<h2 id=\"jitter-has-a-published-formula-so-check-its-bounds\">Jitter has a published formula, so check its bounds</h2>\n\n<p>Jitter is where people give up and go back to counting calls, because the schedule is no longer a fixed list. It does not have to be, because the formula is documented. <code class=\"language-text\">wait_exponential_jitter(initial=1, max=..., exp_base=2, jitter=1)</code> states it outright: \"the wait time is <code class=\"language-text\">min(initial * 2**n + random.uniform(0, jitter), maximum)</code> where <code class=\"language-text\">n</code> is the retry count.\"</p>\n\n<p>From that you get an exact interval per attempt without knowing the random value at all:</p>\n\n<pre class=\"language-python\"><code class=\"language-python\">def test_jittered_backoff_stays_within_its_documented_interval():\n    sleeps: list[float] = []\n    transport = Mock(side_effect=[ServiceUnavailable(\"503\")] * 5)\n\n    with pytest.raises(ServiceUnavailable):\n        Retrying(\n            sleep=sleeps.append,\n            wait=wait_exponential_jitter(initial=0.5, exp_base=2,\n                                         jitter=0.25, max=4),\n            stop=stop_after_attempt(5),\n            reraise=True,\n        )(transport)\n\n    for n, actual in enumerate(sleeps):\n        base = min(0.5 * 2 ** n, 4)\n        # The floor is the un-jittered term; the ceiling adds the jitter,\n        # and the cap applies to the sum.\n        assert base &lt;= actual &lt;= min(base + 0.25, 4)</code></pre>\n\n<p>This is a better test than pinning <code class=\"language-text\">random.uniform</code> to a fixed value would be, because it holds for every draw rather than for one. Patching the random source is still the right move when you need a specific value (a retry that must land exactly on a deadline boundary, say), but bounds are the default.</p>\n\n<p>Which jitter strategy to use is a design question the docs answer directly, and the answer is more specific than \"add jitter.\" <code class=\"language-text\">wait_exponential</code>'s intervals \"are fixed (i.e. there is no jitter), so this strategy is suitable for balancing retries against latency when a required resource is unavailable for an unknown duration, but not suitable for resolving contention between multiple processes for a shared resource. Use <code class=\"language-text\">wait_random_exponential</code> for the latter case.\"</p>\n\n<p>Read that as a decision rule. One client waiting for a service to come back does not need jitter. Forty workers hammering one row lock do, because without it they retry in lockstep forever. If your retry exists to survive contention and its strategy has no jitter, that is a bug the bounds test above will never catch, so it belongs in a review checklist, not a test.</p>\n\n<h2 id=\"deadlines-measure-elapsed-time-not-sleeps\">Deadlines measure elapsed time, not sleeps</h2>\n\n<p>Here is where the recorder alone stops being enough. <code class=\"language-text\">stop_after_delay(max_delay)</code> is documented as \"stop when the time from the first attempt &gt;= limit\". It reads a clock. A sleeper that only appends to a list advances no clock, so under a naive recorder the deadline never fires and a deadline test silently becomes an attempt-count test.</p>\n\n<p>You could patch whatever clock <code class=\"language-text\">stop_after_delay</code> consults, but that is an internal you would be pinning to a version. tenacity offers a supported alternative: a stop rule may be any callable. The docs specify the contract: <code class=\"language-text\">my_stop(retry_state)</code> takes \"info about current retry invocation\" and returns \"whether or not retrying should stop\" as a <code class=\"language-text\">bool</code>. So the deadline can read a clock you own:</p>\n\n<pre class=\"language-python\"><code class=\"language-python\">class FakeClock:\n    \"\"\"A clock that only moves when the code under test sleeps.\"\"\"\n\n    def __init__(self) -&gt; None:\n        self.now = 0.0\n        self.sleeps: list[float] = []\n\n    def sleep(self, seconds: float) -&gt; None:\n        self.sleeps.append(seconds)\n        self.now += seconds\n\n    def deadline(self, limit: float):\n        \"\"\"A tenacity stop callable enforcing `limit` against this clock.\"\"\"\n        return lambda retry_state: self.now &gt;= limit</code></pre>\n\n<pre class=\"language-python\"><code class=\"language-python\">def test_gives_up_at_the_deadline_rather_than_the_attempt_limit():\n    clock = FakeClock()\n    transport = Mock(side_effect=[ServiceUnavailable(\"503\")] * 20)\n\n    with pytest.raises(ServiceUnavailable):\n        Retrying(\n            sleep=clock.sleep,\n            wait=wait_exponential(multiplier=0.5, exp_base=2, max=8),\n            # Both limits, so whichever binds first is explicit.\n            stop=stop_any(clock.deadline(3.0), stop_after_attempt(20)),\n            reraise=True,\n        )(transport)\n\n    assert clock.sleeps == [0.5, 1.0, 2.0]   # 3.5s elapsed at the third wake\n    assert transport.call_count == 4          # the deadline stopped it, not the cap</code></pre>\n\n<p>The last two assertions together are the point: the test proves the <em>deadline</em> ended the retry loop and not the attempt cap. Assert one without the other and the test cannot tell you which rule fired. <code class=\"language-text\">stop_any</code> and <code class=\"language-text\">stop_all</code> exist for exactly this combination: \"stop if any of the stop condition is valid\" and \"stop if all the stop conditions are valid\", and stating both limits explicitly is what makes the question answerable.</p>\n\n<p>In production the same policy uses <code class=\"language-text\">stop_after_delay(3.0)</code>, which measures against a real clock. Keeping the limit a parameter of the policy rather than a literal inside it is what lets the test substitute its own rule without reaching into the library.</p>\n\n<h2 id=\"the-exception-your-caller-sees-is-not-the-one-you-raised\">The exception your caller sees is not the one you raised</h2>\n\n<p>This one bites during a real incident, which is the worst time to discover it. tenacity's default is not to re-raise: \"normally when your function fails its final time (and will not be retried again based on your settings), a <code class=\"language-text\">RetryError</code> is raised. The exception your code encountered will be shown somewhere in the middle of the stack trace.\"</p>\n\n<p>So a caller writing <code class=\"language-text\">except ServiceUnavailable</code> catches nothing, and an alert grouping on exception type groups every retry exhaustion together regardless of cause. The switch is documented alongside it: \"if you would rather see the exception your code encountered at the end of the stack trace (where it is most visible), you can set <code class=\"language-text\">reraise=True</code>.\"</p>\n\n<p>Test whichever one you have chosen, because both are defensible and the silent one is wrong:</p>\n\n<pre class=\"language-python\"><code class=\"language-python\"># With reraise=True — the caller sees the real failure and never has to\n# import tenacity. This is usually right at a library boundary.\ndef test_final_failure_surfaces_the_transport_error():\n    transport = Mock(side_effect=[ServiceUnavailable(\"503\")] * 3)\n\n    with pytest.raises(ServiceUnavailable) as excinfo:\n        Retrying(sleep=lambda _: None, stop=stop_after_attempt(3),\n                 reraise=True)(transport)\n\n    assert \"503\" in str(excinfo.value)\n\n\n# Without it — assert the wrapper, then reach through it, so the test still\n# pins the cause rather than accepting any exhaustion.\ndef test_final_failure_retains_the_cause_inside_retryerror():\n    transport = Mock(side_effect=[ServiceUnavailable(\"503\")] * 3)\n\n    with pytest.raises(RetryError) as excinfo:\n        Retrying(sleep=lambda _: None, stop=stop_after_attempt(3))(transport)\n\n    cause = excinfo.value.last_attempt.exception()\n    assert isinstance(cause, ServiceUnavailable)</code></pre>\n\n<h2 id=\"prove-that-unsafe-failures-are-not-retried\">Prove that unsafe failures are not retried</h2>\n\n<p>The retry predicate is the behaviour most suites never test, and the one most likely to cause damage. Retrying a <code class=\"language-text\">400</code> wastes time; retrying a non-idempotent <code class=\"language-text\">POST</code> creates duplicate charges.</p>\n\n<p>The negative test is three lines and it is the cheapest high-value test in this article:</p>\n\n<pre class=\"language-python\"><code class=\"language-python\">@pytest.mark.parametrize(\n    \"error\",\n    [\n        pytest.param(BadRequest(\"400\"), id=\"client-error-is-not-transient\"),\n        pytest.param(Unauthorized(\"401\"), id=\"auth-error-will-not-self-heal\"),\n        pytest.param(Conflict(\"409\"), id=\"conflict-needs-a-new-request\"),\n    ],\n)\ndef test_does_not_retry_permanent_failures(error):\n    sleeps: list[float] = []\n    transport = Mock(side_effect=error)\n\n    with pytest.raises(type(error)):\n        Retrying(\n            sleep=sleeps.append,\n            wait=wait_exponential(multiplier=0.5),\n            stop=stop_after_attempt(4),\n            retry=retry_if_exception_type(ServiceUnavailable),\n            reraise=True,\n        )(transport)\n\n    assert sleeps == []                 # nothing waited\n    assert transport.call_count == 1    # nothing repeated</code></pre>\n\n<p><code class=\"language-text\">assert sleeps == []</code> is the assertion that matters. A call count of one can also mean the retry never ran for an unrelated reason; an empty sleep list says the policy evaluated the predicate and declined.</p>\n\n<h2 id=\"what-to-record-when-a-retry-runs-in-production\">What to record when a retry runs in production</h2>\n\n<p>Tests establish the schedule. Production needs to show it, and tenacity exposes both halves.</p>\n\n<p>The decorated function carries its own counters: \"you can access the statistics about the retry made over a function by using the <code class=\"language-text\">retry</code> attribute attached to the function and its <code class=\"language-text\">statistics</code> attribute\": <code class=\"language-text\">fetch_order.retry.statistics</code> after a call. Useful in a test as a cross-check, and useful in an incident as a cheap read.</p>\n\n<p>For logs, the callback you want is the one that fires only when another attempt is coming. The docs name it precisely: \"it's also possible to only log failures that are going to be retried. Normally retries happen after a wait interval, so the keyword argument is called <code class=\"language-text\">before_sleep</code>.\"</p>\n\n<pre class=\"language-python\"><code class=\"language-python\">@retry(\n    stop=stop_any(stop_after_delay(3.0), stop_after_attempt(5)),\n    wait=wait_exponential_jitter(initial=0.5, jitter=0.25, max=4),\n    retry=retry_if_exception_type(ServiceUnavailable),\n    reraise=True,\n    # Fires per retried failure, not per attempt: one line per wait.\n    before_sleep=before_sleep_log(logger, logging.DEBUG),\n)\ndef fetch_order(order_id: str) -&gt; dict: ...</code></pre>\n\n<p>There are matching <code class=\"language-text\">before</code> and <code class=\"language-text\">after</code> callbacks with <code class=\"language-text\">before_log</code> and <code class=\"language-text\">after_log</code> helpers if you want every attempt rather than every retry. <code class=\"language-text\">before_sleep</code> is the one that gives you a log line per wait, which is what makes a production schedule reconstructable after the fact.</p>\n\n<h2 id=\"apply-this-now\">Apply this now</h2>\n\n<p>Search your retry tests for <code class=\"language-text\">call_count</code> and for anything that patches <code class=\"language-text\">time.sleep</code>. Both are signals that the schedule is untested. Replace the patch with an injected sleeper and the count with an equality assertion on the recorded list. The second change usually deletes the first assertion rather than joining it.</p>\n\n<p>Then check the two defaults. Anywhere a retry policy is constructed, confirm <code class=\"language-text\">stop</code> and <code class=\"language-text\">wait</code> are both passed explicitly, because the library's defaults are retry-forever and wait-never. And confirm <code class=\"language-text\">retry=</code> names a specific exception type rather than being left to catch everything.</p>\n\n<p>Done looks like a retry test file that runs in under a second and contains, for each policy, four assertions: the exact sleep list, the stop rule that fired, an empty sleep list for a permanent error, and the exception type a caller will actually catch.</p>\n\n<h2 id=\"questions-about-testing-retry-policies\">Questions about testing retry policies</h2>\n\n<h3 id=\"is-freezing-the-clock-a-better-approach\">Is freezing the clock a better approach?</h3>\n\n<p>A clock-freezing library will make the deadline test work, and it is heavier than it needs to be here. The <code class=\"language-text\">FakeClock</code> above is five lines, moves only when the code under test sleeps, and makes the coupling between sleeping and elapsed time explicit, which is the thing the test is about. Reach for a general time-freezing tool when the code under test reads wall-clock time for its own reasons, such as formatting timestamps into a payload, not merely because it retries.</p>\n\n<h3 id=\"should-the-retry-policy-live-with-the-client-or-the-caller\">Should the retry policy live with the client or the caller?</h3>\n\n<p>Separate the policy object from the transport, and let the caller supply it. That is what makes the tests in this article possible without a fake HTTP server, and it also means a batch job and a request handler can use the same client with different deadlines, which they should, since a user-facing request cannot afford a three-second retry budget that a nightly job welcomes.</p>\n\n<h3 id=\"how-do-i-test-the-async-version\">How do I test the async version?</h3>\n\n<p>The same way, with an awaitable recorder. <code class=\"language-text\">AsyncRetrying</code>'s sleeper is typed <code class=\"language-text\">Callable[[float], Awaitable[Any]]</code>, so an <code class=\"language-text\">async def</code> that appends and returns satisfies it. Everything else (the schedule assertion, the bounds check, the empty-list negative test) is unchanged, which is the payoff of injecting rather than patching: the technique does not care whether the sleep was blocking.</p>\n\n<h3 id=\"what-about-the-server-telling-me-when-to-retry\">What about the server telling me when to retry?</h3>\n\n<p>If the API returns a retry hint, the policy must read it, and that is a different wait strategy rather than a tweak to an exponential one: the schedule now depends on the response, so it belongs in a custom wait callback that receives the retry state. Test it the same way: script the transport to return specific hints and assert the recorded sleeps match them. The bug this catches is a policy that computes exponential backoff and ignores the header entirely, which no call-count test can see.</p>\n\n<h3 id=\"does-any-of-this-apply-if-i-wrote-the-retry-loop-myself\">Does any of this apply if I wrote the retry loop myself?</h3>\n\n<p>All of it, and the first change is the same: make the sleeper a parameter with a default rather than a direct call. A hand-rolled loop that calls <code class=\"language-text\">time.sleep(delay)</code> inline cannot be tested without patching a global; one that calls <code class=\"language-text\">self._sleep(delay)</code> can. The rest of the article is then about which four things to assert, and those are independent of whose retry loop it is.</p>\n\n<h2 id=\"primary-references\">Primary references</h2>\n\n<ul>\n<li><a href=\"https://tenacity.readthedocs.io/en/latest/api.html#tenacity.Retrying\">tenacity — API: Retrying and AsyncRetrying</a>. That <code class=\"language-text\">sleep</code> is a constructor parameter typed <code class=\"language-text\">Callable[[Union[int, float]], None]</code> defaulting to the real <code class=\"language-text\">sleep</code>, which is the injection point this article is built on; that <code class=\"language-text\">AsyncRetrying</code> takes <code class=\"language-text\">Callable[[float], Awaitable[Any]]</code>; and the remaining defaults, including <code class=\"language-text\">stop=stop_never</code>, <code class=\"language-text\">wait=wait_none</code>, <code class=\"language-text\">reraise=False</code> and <code class=\"language-text\">retry_error_cls=RetryError</code>.</li>\n<li><a href=\"https://tenacity.readthedocs.io/en/latest/api.html#wait-functions\">tenacity — API: Wait Functions</a>. The <code class=\"language-text\">wait_exponential</code> signature including its effectively unbounded default <code class=\"language-text\">max</code>, and its guidance that fixed intervals are \"suitable for balancing retries against latency when a required resource is unavailable for an unknown duration, but not suitable for resolving contention between multiple processes for a shared resource\"; the <code class=\"language-text\">wait_exponential_jitter</code> formula <code class=\"language-text\">min(initial * 2**n + random.uniform(0, jitter), maximum)</code>; and <code class=\"language-text\">wait_none</code>, <code class=\"language-text\">wait_fixed</code>, <code class=\"language-text\">wait_random</code>, <code class=\"language-text\">wait_chain</code> and <code class=\"language-text\">wait_combine</code>.</li>\n<li><a href=\"https://tenacity.readthedocs.io/en/latest/api.html#stop-functions\">tenacity — API: Stop Functions</a>. That <code class=\"language-text\">stop_after_attempt</code> stops \"when the previous attempt &gt;= max_attempt\" while <code class=\"language-text\">stop_after_delay</code> stops \"when the time from the first attempt &gt;= limit\", a clock reading rather than a sleep count, and that <code class=\"language-text\">stop_any</code> and <code class=\"language-text\">stop_all</code> combine conditions with any and all semantics.</li>\n<li><a href=\"https://tenacity.readthedocs.io/en/latest/index.html#error-handling\">tenacity — Error Handling</a>. That on final failure \"a <code class=\"language-text\">RetryError</code> is raised\" and \"the exception your code encountered will be shown somewhere in the middle of the stack trace\", and that <code class=\"language-text\">reraise=True</code> instead surfaces it \"at the end of the stack trace (where it is most visible)\".</li>\n<li><a href=\"https://tenacity.readthedocs.io/en/latest/index.html#statistics\">tenacity — Statistics</a>. That statistics for a decorated function are reachable \"by using the <code class=\"language-text\">retry</code> attribute attached to the function and its <code class=\"language-text\">statistics</code> attribute\", which makes the attempt record readable both in a test and during an incident.</li>\n<li><a href=\"https://tenacity.readthedocs.io/en/latest/index.html#before-and-after-retry-and-logging\">tenacity — Before and After Retry, and Logging</a>. The <code class=\"language-text\">before</code>, <code class=\"language-text\">after</code> and <code class=\"language-text\">before_sleep</code> callbacks with their <code class=\"language-text\">before_log</code>, <code class=\"language-text\">after_log</code> and <code class=\"language-text\">before_sleep_log</code> helpers, and the stated reason for the third's name: it logs \"only … failures that are going to be retried\", because \"normally retries happen after a wait interval\".</li>\n<li><a href=\"https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.side_effect\">Python — unittest.mock: side_effect</a>. That \"if <code class=\"language-text\">side_effect</code> is an iterable then each call to the mock will return the next value from the iterable\", and that an exception class or instance in that position is raised when the mock is called. This is what allows one list to script a sequence of failures followed by a success.</li>\n<li><a href=\"https://tenacity.readthedocs.io/en/latest/index.html#other-custom-callbacks\">tenacity — Other Custom Callbacks</a>. The contract a custom stop rule must satisfy: <code class=\"language-text\">my_stop(retry_state)</code> receives \"info about current retry invocation\" and returns a <code class=\"language-text\">bool</code> for \"whether or not retrying should stop\". This is how a deadline can be enforced against a clock the test owns without patching library internals. The same section documents the equivalent hooks for <code class=\"language-text\">wait</code>, <code class=\"language-text\">retry</code>, <code class=\"language-text\">before</code>, <code class=\"language-text\">after</code> and <code class=\"language-text\">before_sleep</code>.</li>\n<li><a href=\"https://docs.pytest.org/en/stable/how-to/monkeypatch.html\">pytest — How to monkeypatch/mock modules and environments</a>. That <code class=\"language-text\">monkeypatch</code> modifications \"will be undone after the requesting test or fixture has finished\", the property that makes patching safe but still global for the duration, which is why an injected sleeper is preferable when the library already accepts one.</li>\n</ul>\n"}