Claude Code for Tenacity: Python Retry Logic — Claude Skills 360 Blog
Blog / AI / Claude Code for Tenacity: Python Retry Logic
AI

Claude Code for Tenacity: Python Retry Logic

Published: December 7, 2027
Read time: 5 min read
By: Claude Skills 360

Tenacity provides powerful retry logic for Python functions. pip install tenacity. from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type. Basic: @retry(stop=stop_after_attempt(3)). Exponential backoff: @retry(wait=wait_exponential(multiplier=1, min=1, max=60)). Fixed wait: @retry(wait=wait_fixed(2)). Jitter: @retry(wait=wait_exponential_jitter(initial=1, max=60)). Combined: @retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=2, min=2, max=30)). Condition: @retry(retry=retry_if_exception_type(ConnectionError)). Multiple: @retry(retry=retry_if_exception_type((TimeoutError, ConnectionError))). Result: @retry(retry=retry_if_result(lambda r: r is None)). Not result: @retry(retry=retry_if_not_result(lambda r: r.ok)). Before sleep: from tenacity import before_sleep_log; @retry(before_sleep=before_sleep_log(logger, logging.WARNING)). Stop delay: stop=stop_after_delay(60). Combined stop: stop=stop_after_attempt(5) | stop_after_delay(120). Reraise: @retry(reraise=True) — raises original exception instead of RetryError. RetryError: from tenacity import RetryError — catch when all attempts exhausted. TryAgain: raise tenacity.TryAgain inside function to force retry. Async: from tenacity import AsyncRetrying, async with AsyncRetrying(stop=stop_after_attempt(3)) as r: async for attempt in r: with attempt: result = await coro(). Custom: @retry(after=lambda r: print(f"retry {r.attempt_number}")). Statistics: fn.retry.statistics. Claude Code generates Tenacity retry decorators for HTTP clients, database connections, and external API integrations.

CLAUDE.md for Tenacity

## Tenacity Stack
- Version: tenacity >= 8.2
- Decorator: @retry(stop=..., wait=..., retry=..., before_sleep=...)
- Stop: stop_after_attempt(N) | stop_after_delay(secs) | combine with |
- Wait: wait_fixed | wait_exponential(mult, min, max) | wait_exponential_jitter
- Retry on: retry_if_exception_type(ExcClass) | retry_if_result(fn)
- Logging: before_sleep=before_sleep_log(logger, logging.WARNING)
- Reraise: reraise=True to surface original exception (not RetryError)
- Async: AsyncRetrying context manager for coroutines

Tenacity Retry Pipeline

# utils/tenacity_pipeline.py — resilient retry logic with Tenacity
from __future__ import annotations
import asyncio
import logging
import random
import time
from functools import wraps
from typing import Any, Callable, Type

import tenacity
from tenacity import (
    retry,
    retry_if_exception_type,
    retry_if_not_result,
    retry_if_result,
    stop_after_attempt,
    stop_after_delay,
    wait_exponential,
    wait_exponential_jitter,
    wait_fixed,
    wait_random,
    before_sleep_log,
    after_log,
    RetryError,
    TryAgain,
    AsyncRetrying,
)

logger = logging.getLogger(__name__)


# ── 0. Pre-configured decorators ─────────────────────────────────────────────

def http_retry(
    max_attempts: int   = 5,
    min_wait:     float = 1.0,
    max_wait:     float = 60.0,
    multiplier:   float = 2.0,
    reraise:      bool  = True,
):
    """
    Standard HTTP retry: exponential back-off with jitter, 5 attempts max.
    Retries on ConnectionError, TimeoutError, and OSError (transient network).
    """
    return retry(
        stop=stop_after_attempt(max_attempts),
        wait=wait_exponential_jitter(initial=min_wait, max=max_wait, exp_base=multiplier),
        retry=retry_if_exception_type((ConnectionError, TimeoutError, OSError)),
        before_sleep=before_sleep_log(logger, logging.WARNING),
        reraise=reraise,
    )


def database_retry(
    max_attempts: int = 3,
    wait_seconds: float = 2.0,
    reraise: bool = True,
):
    """
    Short fixed-wait retry for transient database errors.
    Retries on common operational exceptions; does NOT retry programming errors.
    """
    return retry(
        stop=stop_after_attempt(max_attempts),
        wait=wait_fixed(wait_seconds),
        retry=retry_if_exception_type((OSError, ConnectionError, TimeoutError)),
        before_sleep=before_sleep_log(logger, logging.WARNING),
        reraise=reraise,
    )


def api_retry(
    max_attempts:    int = 4,
    retry_on_status: tuple[int, ...] = (429, 500, 502, 503, 504),
    reraise: bool = True,
):
    """
    Retry decorator for HTTP API calls that return response objects.
    Both raises exceptions AND retries on specific HTTP status codes.
    Pass your response object — retry_if_result checks .status_code.
    """
    def _should_retry_response(r) -> bool:
        if r is None:
            return True
        status = getattr(r, "status_code", None) or getattr(r, "status", None)
        return status in retry_on_status

    return retry(
        stop=stop_after_attempt(max_attempts),
        wait=wait_exponential_jitter(initial=1, max=30),
        retry=(
            retry_if_exception_type((ConnectionError, TimeoutError)) |
            retry_if_result(_should_retry_response)
        ),
        before_sleep=before_sleep_log(logger, logging.WARNING),
        reraise=reraise,
    )


# ── 1. Async retry helpers ────────────────────────────────────────────────────

async def retry_async(
    coro_fn:      Callable,
    *args,
    max_attempts: int   = 5,
    min_wait:     float = 1.0,
    max_wait:     float = 60.0,
    exceptions:   tuple = (Exception,),
    **kwargs,
) -> Any:
    """
    Retry an async coroutine with exponential back-off.
    Alternative to @retry for when you can't decorate the function directly.
    """
    async for attempt in AsyncRetrying(
        stop=stop_after_attempt(max_attempts),
        wait=wait_exponential_jitter(initial=min_wait, max=max_wait),
        retry=retry_if_exception_type(exceptions),
        before_sleep=before_sleep_log(logger, logging.WARNING),
        reraise=True,
    ):
        with attempt:
            return await coro_fn(*args, **kwargs)


def async_http_retry(
    max_attempts: int   = 5,
    min_wait:     float = 1.0,
    max_wait:     float = 60.0,
):
    """Async version of http_retry decorator."""
    return retry(
        stop=stop_after_attempt(max_attempts),
        wait=wait_exponential_jitter(initial=min_wait, max=max_wait),
        retry=retry_if_exception_type((ConnectionError, TimeoutError, OSError)),
        before_sleep=before_sleep_log(logger, logging.WARNING),
        reraise=True,
    )


# ── 2. Conditional retry patterns ─────────────────────────────────────────────

def retry_until_truthy(
    fn:           Callable,
    *args,
    max_attempts: int   = 10,
    wait_seconds: float = 5.0,
    **kwargs,
) -> Any:
    """
    Keep calling fn until it returns a truthy value.
    Useful for polling: wait until job is complete, resource is available, etc.
    """
    @retry(
        stop=stop_after_attempt(max_attempts),
        wait=wait_fixed(wait_seconds),
        retry=retry_if_result(lambda r: not r),
        before_sleep=before_sleep_log(logger, logging.INFO),
        reraise=False,
    )
    def _call():
        return fn(*args, **kwargs)

    try:
        return _call()
    except RetryError:
        return None


def retry_until_not_none(
    fn:           Callable,
    *args,
    max_attempts: int   = 5,
    wait_seconds: float = 2.0,
    **kwargs,
) -> Any:
    """Poll fn until it returns a non-None value or max_attempts is exhausted."""
    @retry(
        stop=stop_after_attempt(max_attempts),
        wait=wait_fixed(wait_seconds),
        retry=retry_if_result(lambda r: r is None),
        reraise=False,
    )
    def _call():
        return fn(*args, **kwargs)

    try:
        return _call()
    except RetryError:
        return None


# ── 3. Usage examples ─────────────────────────────────────────────────────────

@http_retry(max_attempts=4, min_wait=1.5, max_wait=30.0)
def fetch_url(url: str, timeout: float = 10.0) -> dict:
    """
    HTTP GET with retry. In production, replace urllib.request with httpx/requests.
    """
    import urllib.request, json
    with urllib.request.urlopen(url, timeout=timeout) as resp:
        return json.loads(resp.read())


@database_retry(max_attempts=3, wait_seconds=1.0)
def query_database(connection, sql: str, params: tuple = ()) -> list:
    """Database query with retry on transient connection errors."""
    cursor = connection.cursor()
    cursor.execute(sql, params)
    return cursor.fetchall()


@retry(
    stop=stop_after_attempt(3) | stop_after_delay(120),
    wait=wait_exponential(multiplier=1, min=2, max=30),
    retry=retry_if_exception_type(Exception),
    before_sleep=before_sleep_log(logger, logging.WARNING),
    after=after_log(logger, logging.DEBUG),
    reraise=True,
)
def call_external_service(payload: dict) -> dict:
    """
    Call an external service with combined stop condition:
    give up after 3 attempts OR after 2 minutes, whichever comes first.
    """
    # simulate a call that sometimes fails
    if random.random() < 0.4:
        raise ConnectionError("Service temporarily unavailable")
    return {"status": "ok", "payload": payload}


@async_http_retry(max_attempts=3, min_wait=0.5, max_wait=10.0)
async def async_fetch(session, url: str) -> dict:
    """Async HTTP fetch with retry (use with aiohttp ClientSession)."""
    async with session.get(url) as response:
        response.raise_for_status()
        return await response.json()


# ── 4. Retry with TryAgain for custom logic ───────────────────────────────────

@retry(stop=stop_after_attempt(5), wait=wait_exponential(min=1, max=30), reraise=True)
def smart_api_call(client, resource_id: str) -> dict:
    """
    Use TryAgain for manual retry signaling inside the function body.
    Useful when retry condition requires parsing the response (not just exceptions).
    """
    response = client.get(f"/resources/{resource_id}")
    if response.status_code == 202:
        # Resource still processing — retry
        logger.info("Resource %s still processing, retrying...", resource_id)
        raise TryAgain
    if response.status_code == 200:
        return response.json()
    raise RuntimeError(f"Unexpected status: {response.status_code}")


# ── 5. Retry statistics ────────────────────────────────────────────────────────

def with_retry_stats(fn: Callable) -> Callable:
    """
    Wrap a retried function to log retry statistics after each call.
    """
    @wraps(fn)
    def wrapper(*args, **kwargs):
        try:
            result = fn(*args, **kwargs)
            stats  = fn.retry.statistics
            if stats.get("attempt_number", 1) > 1:
                logger.info(
                    "%s succeeded after %d attempts (%.2fs elapsed)",
                    fn.__name__,
                    stats.get("attempt_number", 1),
                    stats.get("idle_for", 0),
                )
            return result
        except RetryError as exc:
            stats = fn.retry.statistics
            logger.error(
                "%s exhausted %d attempts: %s",
                fn.__name__,
                stats.get("attempt_number", "?"),
                exc,
            )
            raise
    return wrapper


# ── Demo ──────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO,
                        format="%(asctime)s %(levelname)-8s %(message)s")

    print("Tenacity Retry Demo")
    print("=" * 50)

    # Simulate a flaky function
    call_count = [0]

    @retry(
        stop=stop_after_attempt(5),
        wait=wait_exponential_jitter(initial=0.1, max=1.0),
        retry=retry_if_exception_type(ValueError),
        before_sleep=before_sleep_log(logger, logging.INFO),
        reraise=True,
    )
    def flaky_fn():
        call_count[0] += 1
        if call_count[0] < 3:
            raise ValueError(f"Attempt {call_count[0]} failed")
        return f"Success on attempt {call_count[0]}"

    try:
        result = flaky_fn()
        print(f"\nResult: {result}")
        print(f"Statistics: {flaky_fn.retry.statistics}")
    except RetryError as e:
        print(f"All attempts exhausted: {e}")

    # Polling example
    print("\nPolling until truthy:")
    state = {"ready": False, "tick": 0}

    def check_ready():
        state["tick"] += 1
        if state["tick"] >= 3:
            state["ready"] = True
        return state["ready"]

    result = retry_until_truthy(check_ready, max_attempts=5, wait_seconds=0.01)
    print(f"  Ready after {state['tick']} poll(s): {result}")

For the custom time.sleep + try/except loop alternative — manual retry loops duplicate boilerplate across every function, increment attempt counters by hand, and silently swallow exceptions when tired while Tenacity’s @retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(...)) centralizes retry policy, computes jittered back-off, logs each attempt via before_sleep_log, and surfaces the original exception with reraise=True — removing 20 lines of error-prone manual retry code with a single decorator. For the backoff library alternative — backoff has @backoff.on_exception but no retry_if_result predicate (cannot retry on HTTP 429 without raising), no combined stop conditions (stop_after_attempt | stop_after_delay), and no AsyncRetrying context manager, while Tenacity’s retry_if_result(lambda r: r.status_code in (429, 503)) retries on response status without raising, enabling the same decorator for exception-based and result-based policies. The Claude Skills 360 bundle includes Tenacity skill sets covering http_retry/database_retry/api_retry pre-built decorators, stop_after_attempt and stop_after_delay combination, wait_exponential_jitter, retry_if_exception_type / retry_if_result, before_sleep_log, TryAgain for manual retry, AsyncRetrying for coroutines, retry statistics, and retry_until_truthy polling pattern. Start with the free tier to try retry logic code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free