CODE VERIFIED

Ruff 0.17.3 Makes UP032 Skip Unconvertible str.format Calls

Ruff 0.17.3 narrows UP032 autofixes, correcting signed format-field parsing and skipping rewrites that could change lookup or exception behavior.

Oct 28, 2026, 12:25 am PDT ยท 6 min read
Concept diagram showing UP032 routing representable format calls to f-strings and skipping unrepresentable calls.

Quick Answer

Ruff 0.17.3 narrows the cases where UP032 suggests replacing a str.format call with an f-string. The official release notes, dated October 26, 2026, list the change as a bug fix for incorrect conversions. The rule now skips several field forms that its conversion builder cannot represent accurately, while retaining supported rewrites.

The concrete distinction is small but consequential: +0 in a format field is a keyword name, while +1 inside an element lookup is a string key. The parser change distinguishes these cases. The merged change corrects that parsing boundary and adds guards around f-string construction.

This is a released change to lint and autofix eligibility. It is not evidence that every UP032 transformation preserves every aspect of program behavior.

In Plain English

A format field and an f-string expression can look alike without meaning the same thing. Python’s format-string grammar distinguishes digit-only argument names from keyword names, and permits element indices to be digits or string indices. A converter must carry those distinctions into the Python expression it generates.

For example, a leading plus sign can be part of a name rather than an instruction to use a positive integer. Treating that name as an index changes the lookup. Other field forms create a different problem: an attribute may not be a valid identifier, or a key may contain a quote or backslash that the conversion builder cannot safely emit in its chosen syntax.

Preserving an exception also matters. A rewrite that successfully produces text can still change behavior if the original call would have failed. The patch explicitly handles mixed automatic and manual field numbering on that basis: its code comment explains that the original format call raises ValueError, whereas the corresponding f-string would succeed. These are observed guard conditions in the PR diff, rather than a general claim about all string conversions.

Code Evidence

The parser stops accepting signed names as numeric indices

The merged diff replaces direct unsigned-integer parsing with a dedicated helper. These are exact, shortened branch excerpts from the commit; they are not complete functions.

Before:

} else if let Ok(index) = first.parse::<usize>() {

After:

} else if let Some(index) = parse_index(&first) {

The helper accepts only non-empty strings consisting entirely of ASCII digits before attempting integer parsing. The same distinction is applied to bracket indices. The commit explains why the extra check is necessary: Rust’s unsigned-integer parser accepts a leading plus sign, but Python treats the field name +0 as a keyword.

Parser assertions in the change cover both sides of that distinction. Parsing +0 produces a keyword field. Parsing 0[+1][-1] produces numeric root index zero followed by the string indices +1 and -1. These are repository test assertions, not results independently reproduced for this article.

A skipped call and a retained conversion

The added UP032 fixture leaves this call without a conversion suggestion:

"{+0}".format(0)

The supplied source describes the field as a keyword name; the fixture records that Ruff skips it. The new fixture establishes that Ruff skips it. It does not show the older erroneous replacement, so no specific previous generated expression can be inferred from this example.

Code comparison showing a signed format field skipped and a signed element key preserved as a quoted string key.
The added fixtures distinguish an unconvertible signed field name from a signed element key that remains eligible for conversion.

A signed element key has a representable equivalent. The added snapshot records this input and expected replacement:

# Input
"{0[+1]}".format(d)

# Expected replacement
f"{d['+1']}"

The plus sign stays inside the quoted key. That is the essential behavior demonstrated by the snapshot: the generated expression performs a string-key lookup. The patch therefore does more than suppress suggestions; it also preserves a conversion when the original field can be expressed correctly.

The conversion builder rejects additional field forms

The conversion-rule changes reject attributes that are not identifiers. They also reject string keys containing either the quote selected for the generated f-string or a backslash. Conversion flags are restricted to s, r, and a, the forms documented by Python. Mixed automatic and manual numbering causes an early return without a suggestion.

The fixture adds skipped examples across those boundaries, including unsupported conversions and problematic attribute or key syntax. It also retains a valid representation conversion using !r. Read together, the guards and fixtures show a selective eligibility change, rather than a blanket withdrawal of UP032 fixes.

Why It Matters

Ruff’s UP032 documentation describes a rule that checks format calls replaceable by f-strings and says a fix is sometimes available. This patch makes that conditional availability more concrete: a style suggestion must first pass the converter’s representation checks.

For teams applying UP032 fixes through editor actions, commands, or CI, the practical implication is that the newly covered unconvertible forms are left without a rewrite. That conclusion follows from the changed guards and expected snapshots. It is not a measured reduction in failures, and the sources provide no benchmark or incident data for this fix.

The signed-key example is particularly useful when reviewing an automated change. An output that looks more like ordinary Python is not enough; it must preserve whether the original field selected a positional argument, a keyword, or a string key. The added tests make that distinction visible at the point where a lint suggestion becomes replacement code.

There is also a practical limit to interpreting a missing suggestion. For the newly listed cases, leaving a call unchanged is the intended result of these checks. It does not imply that the format call itself is valid, or that running it will succeed. The signed field example remains unchanged; the fixture records no conversion suggestion.

An Open Question

Which remaining behavior-preservation cases from issue #15874 are covered by other UP032 changes, and which still lack a safe fix? PR #25485 explicitly describes a partial fix. Its scope cannot establish the status of every example in that broader issue.

Resolving that question requires an example-by-example map from the issue to relevant merged changes and current expected tests, including the cases involving evaluation order, side effects, unused arguments, and exceptions. The supplied research establishes this patch’s boundaries but does not supply that complete map.

Scope and Limitations

The verified release record places this fix in Ruff 0.17.3, released October 26, 2026. The PR merged on October 23. The supplied research identifies three changed files: the UP032 markdown fixture, the f-string conversion rule, and the format-string parser. It provides an exact path for the fixture: crates/ruff_linter/resources/mdtest/pyupgrade/f-string.md.

The supplied research reports that the PR test plan names the new UP032 mdtest and parser unit tests, and that GitHub reports 53 passed checks. These are repository-reported checks; no code, local tests, benchmarks, or independent runtime reproduction were performed for the supplied research or this article.

The evidence supports a focused parser and conversion correction. It does not establish exhaustive behavior preservation across Python expressions, custom objects, versions, or configurations. The broader issue includes hazards beyond the field forms handled here, so release availability should not be read as a complete resolution of UP032 soundness concerns.

Sources