Skip to content

Unreachable code in before_log reached #511

Open

Description

@zdebra

Hello, I've reached code that is marked as unreachable in before_log function:

import logging

from tenacity import RetryError, Retrying, before_log

logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)


def f():
    try:
        for attempt in Retrying(
            before=before_log(logger, logging.DEBUG),
        ):
            with attempt:
                raise Exception("hello")
    except RetryError as exc:
        exc.reraise()


f()

This code sample produces:

2025-01-28 15:11:42,725 - DEBUG - Starting call to '<unknown>', this is the 72232nd time calling it.
2025-01-28 15:11:42,725 - DEBUG - Starting call to '<unknown>', this is the 72233rd time calling it.
2025-01-28 15:11:42,725 - DEBUG - Starting call to '<unknown>', this is the 72234th time calling it.
2025-01-28 15:11:42,725 - DEBUG - Starting call to '<unknown>', this is the 72235th time calling it.
2025-01-28 15:11:42,725 - DEBUG - Starting call to '<unknown>', this is the 72236th time calling it.
2025-01-28 15:11:42,725 - DEBUG - Starting call to '<unknown>', this is the 72237th time calling it.

This is because retry_state.fn is probably not being set here.

Python 3.9.19 & tenacity 9.0.0.

Activity

  1. LotfiRafik commented on Feb 16, 2025

    @LotfiRafik

    This comment "# NOTE(sileht): can't really happen, but we must please mypy" is outdated. retry_state.fn can be None in the case of using retry inside context manager. its the expected behaviour not a bug.

  2. Mukller commented on Jul 30, 2026

    @Mukller

    Root cause analysis

    When Retrying is used as a context manager (the for attempt in Retrying(...): with attempt: pattern), __iter__ creates:

    retry_state = RetryCallState(self, fn=None, args=(), kwargs={})

    fn=None because there's no wrapped function — the user's code lives in the with attempt: block. Consequently, get_fn_name() falls through to:

    return str(self.retry_object)  # → self._name or "<unknown>"

    When no name= argument was passed to Retrying(...), self._name is None and __str__ returns "<unknown>". The comment # NOTE(sileht): can't really happen was valid only for the decorator path; it has been removed in the current codebase, but the underlying issue (unhelpful log output) remained.

    Fix (PR linked above)

    Inside __iter__, before the first yield, sys._getframe(1) refers to the frame that called next() on the generator — which is the for loop in the user's code. We capture f_code.co_qualname (Python ≥ 3.11) or f_code.co_name (older) and store it as retry_state._inferred_name.

    get_fn_name() checks _inferred_name before falling back to "<unknown>", so before_log() now logs the actual enclosing function name:

    # Before fix:
    Starting call to '<unknown>', this is the 1st time calling it.
    
    # After fix:
    Starting call to 'my_module.my_function', this is the 1st time calling it.
    

    Users who already pass name="my_name" are completely unaffected — that path is checked first.

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