Skip to content

[FEATURE REQUEST] warn when retried code is not idempotent #504

Open

Description

@Clement-Lelievre

Hi,

in advance: I've looked at the past issues and PRs in this repo, looking for terms like "stateful" and "idempotent" and did not see anything related to my issue. Sorry if I missed it. Also I've had a quick glance at the doc but don't know it inside and out.

The below request might be out of scope for tenacity, because, as will be seen below, the issue is due to a bad pattern in the code I was looking at.
But I'll still share what happened to me today, and maybe that can give rise to a nice feature for tenacity.
I'll start with the result: tedious debugging because the traceback was not showing the actual, relevant issue.

The reason is that the retried code was not idempotent, specifically the decorated method deleted a class attribute, which can work without error at most once, while the nb of retries was 3.

Below is a MRE:

# python==3.12.7, tenacity==9.0.0
from tenacity import stop_after_attempt, retry

class C:
    def __init__(self):
        self.a = ""
        
    @retry(stop=stop_after_attempt(3))
    def f(self):
        del self.a # represents a non idempotent action
        1/0  # represents the actual prod error

C().f()  # AttributeError: 'C' object has no attribute 'a'

In a Machine Learning context, del is quite common to clean up memory and so this is a real use case.
One way to avoid this is to decorate the minimal necessary code and avoid the above pattern, or use features like retry_if_exception_type but I'm wondering if it would be possible for tenacity to issue a warning log whenever there are > 1 error types encountered across all the retries

Activity

  1. Master-Norna commented on Oct 13, 2026

    @Master-Norna

    The first exception can be preserved in the logs using the existing before_sleep_log callback, without changing the retry policy:

    import logging
    from tenacity import before_sleep_log, retry, stop_after_attempt
    
    logging.basicConfig(level=logging.WARNING)
    logger = logging.getLogger(__name__)
    
    class C:
        def __init__(self):
            self.a = ""
    
        @retry(
            stop=stop_after_attempt(3),
            before_sleep=before_sleep_log(logger, logging.WARNING, exc_info=True),
        )
        def f(self):
            del self.a
            1 / 0
    
    C().f()

    I tested this on Python 3.12.14 with both Tenacity 9.0.0 and current main (32ac68f). It logs the first ZeroDivisionError with its traceback, then the intermediate AttributeError. The final RetryError still represents the last AttributeError; adding reraise=True would raise that last exception directly, not recover the first one.

    One limitation: before_sleep only runs when another retry is scheduled, so it does not log the final failed attempt. If you need to examine every failure accepted by the retry predicate, including the exhausted attempt, a custom after callback can inspect retry_state.outcome.exception().

    This gives a workaround for the debugging problem, rather than automatically warning only when exception types change. A change of exception type alone does not establish that an operation is non-idempotent, and repeated exceptions of the same type do not establish that it is safe to retry. Keeping side-effecting cleanup outside the retried section and retrying only the intended transient failures remains the safer fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions