Python’s logging module filters by level before interpolation: logger.debug("Processing %s", value) evaluates value eagerly but skips the %s substitution if DEBUG is below threshold. F-strings give that savings up. logger.debug(f"Processing {value}") builds the final string at the call site, before logging sees it, so disabled-level calls still pay the full interpolation cost. Pylint’s logging-fstring-interpolation flags exactly this anti-pattern.
The argument-evaluation cost is paid either way (both forms pass arguments eagerly), so the per-call savings are only the format work itself. That’s small for simple substitutions but grows with many fields, complex format specs, or repr-heavy types. Truly lazy logging requires an if logger.isEnabledFor(logging.DEBUG): guard around the call site.
PEP 498 defined f-strings as runtime expressions evaluated in the surrounding scope, with no deferred mode. PEP 501 proposed i"" interpolation strings that would defer evaluation specifically for cases like logging, but stayed deferred itself pending experience with PEP 498. PEP 750 (accepted for Python 3.14) generalizes f-strings into t"" template strings that expose a structured template object before formatting, opening a path for logging frameworks to accept templates instead of finished strings and defer the work themselves.