Skip to content

prefer-fstring ​

prefer-fstring converts a %-formatted template or a str.format() call to the f-string that puts each expression inline where it renders, so "%s=%s" % (key, value) reads f"{key}={value}". Each form runs behind its own facet, rewrite-percent covering the % operator and rewrite-str-format covering the method call.

Both facets read target-version and neither runs until it names Python 3.6 or higher, the release that added f-strings. A project with no target-version set keeps every template as written.

Each %s in "%s=%s" % (key, value) reads the tuple member at its own position, the first taking key and the second value. The rewrite moves each expression inline where it renders, so the template and its tuple collapse to f"{key}={value}".

label = f"{key}={value}"
python

Where a Template Holds ​

The rewrite is written only where both forms render the same text, so several kinds of template stay as written.

The % operator has semantics a replacement field does not reproduce, so these templates stay:

  1. A bare right-hand side, because value may be a one-element tuple that % unpacks and a replacement field does not.
  2. A %d, %i, or %u, because it truncates a float where {:d} raises.
  3. A %c, because it maps an ordinal.
  4. A width or precision on %s, since the width renders None where {:8} raises and the precision cuts the rendered text where {:.3} measures the value itself.

An argument no field reads keeps the whole call as written, because removing it would remove its evaluation. An argument two fields read keeps the call whenever evaluating it runs code, since the call evaluates it once where the fields would evaluate it twice.

The template reads x twice, and the call binds x to build(). The line stays as written, because "{x} {x}".format(x=build()) evaluates build() once whereas f"{build()} {build()}" would call it twice, so the rewrite would change behavior.

twice = "{x} {x}".format(x=build())
python

A value the field itself cannot carry keeps the template too, covering a quote matching the delimiter the f-string opens with, a backslash, a line break, and a brace, which are the bounds every Python version accepts. A comment anywhere inside the template or the call keeps it as well, since an f-string has no place for one. A rewrite keeps the template too where its line would run past the budget with every rewrite on that line in place and the padding strip-stranded-padding removes already gone, because no layout rule reaches inside an f-string to wrap it back.

reflow-calls, reflow-signatures, and reflow-collections measure each template this rule converts at the width of its f-string, so a construct that fits on one row once converted lands on that row in the same pass. reflow-calls leaves a call inside such a template whole wherever its f-string fits the row it lands on, and stack-method-chains leaves a chain joined there outside a collection reflow-collections expands and a signature reflow-signatures lays out one parameter per line, since a break inside the template would keep it as written.

At a 50-column budget, the value "%s:%s" % (type(exc), exc) runs "faultString"'s entry past the budget, whereas the f-string prefer-fstring writes for it fits. reflow-collections measures the value at the width of f"{type(exc)}:{exc}", so the entry stays on one row and a second pass finds nothing left to join.

def collect(results, exc):
    results.append(
        {
            "faultCode": 1,
            "faultString": f"{type(exc)}:{exc}"
        }
    )
python

Configuration ​

KeyTypeDefaultMeaning
enabledbooltrueTurns the rule on or off.
rewrite-percentbooltrueConverts printf-style % interpolation to an f-string, so "%s=%s" % (k, v) reads as f"{k}={v}". false leaves every % template in place.
rewrite-str-formatbooltrueConverts a str.format() call to an f-string, so "{}={}".format(k, v) reads as f"{k}={v}". false leaves every str.format() call in place.

The target-version field from the top-level Configuration gates both facets per project, and an unset field keeps every template as written.

Facets ​

Both facets build the same replacement field, so a conversion and a format spec pass through unchanged wherever the f-string grammar matches the template's. The printf flags translate to their format-spec counterparts, so - reads as <.

rewrite-percent ​

rewrite-percent reads the template's specs in order and pairs each one with the value it renders. A tuple literal binds by position, a dict literal of identifier-shaped string keys binds by name through the parenthesized mapping key, and a lone spec also reads a literal right-hand side.

Each %(...)s spec names the dict entry it reads, %(name)s resolving to who and %(age)s to years, whatever order the dict lists them in. The rewrite moves each entry's value into its field and removes the dict, so line becomes f"{who} is {years}".

line = f"{who} is {years}"
python

rewrite-str-format ​

rewrite-str-format resolves each field against the call's arguments, covering the automatic numbering of an empty field, an explicit index, and a keyword name, and it keeps any attribute or index the field name spelled attached to the value inline.

Each field writes an accessor after its argument reference, {0.attr} an attribute and {1[2]} a numeric index. The rewrite reattaches each accessor to the argument it read, so the fields become {record.attr} and {rows[2]}.

detail = f"{record.attr} and {rows[2]}"
python

The Canonical Case ​

Each %s in "%s=%s" % (key, value) reads the tuple member at its own position, the first taking key and the second value. The rewrite moves each expression inline where it renders, so the template and its tuple collapse to f"{key}={value}".

label = f"{key}={value}"
python

More Examples ​

%r renders repr() of its value and %a renders ascii(), the operations an f-string field writes as !r and !a. The rewrite maps each directly, so %r on record becomes {record!r} and %a on name becomes {name!a}.

The generator x for x in xs is the call's only argument, the one position where Python accepts it without parentheses. The rewrite writes the field as f"{(x for x in xs)}", adding the parentheses back, because a bare generator inside a replacement field does not parse.

The .format call writes a !r conversion on record and a :>8 format spec on column. Both pass through unchanged into the fields, so shown becomes f"{record!r} in {column:>8}", because an f-string field reads the same conversion and spec grammar the template did.

In the % template "%.1f%% done", %% is the escaped spelling of one literal percent sign, an escape an f-string does not need. The rewrite collapses %% to a plain % while %.1f becomes the field {ratio:.1f}, its precision carried into the format spec.

In the % template "{outer} %s", {outer} is plain text, because only %s marks a substitution there. The rewrite doubles the literal braces to {{outer}} while %s becomes {inner}, because an f-string reads a bare { as opening a field, and the rendered text stays identical.

The right-hand side of % in tag = "%s" % 42 is the bare literal 42 rather than a tuple. The rewrite writes it straight into the field as f"{42}", because a literal can never be a tuple for % to unpack, so the lone %s reads it directly.

Each %(...)s spec names the dict entry it reads, %(name)s resolving to who and %(age)s to years, whatever order the dict lists them in. The rewrite moves each entry's value into its field and removes the dict, so line becomes f"{who} is {years}".

Each field binds its keyword argument by name, {who} resolving to name and {points} to score, whatever order the call lists them in. The rewrite moves those expressions inline and removes the .format call, so line becomes f"{name} scored {score}".

mask, pad, and left use the %x, %o, %08.3f, and %-10.2f conversions. Each translates to its format-spec counterpart, so %x and %o become {bits:x} and {mode:o}, %08.3f keeps its zero pad, width, and precision in {measure:08.3f}, and the - left-adjust flag, which has no direct counterpart, becomes < in {score:<10.2f} with the trailing | untouched.

pattern = r"\d+%s" % (suffix,) carries an r prefix. The rewrite writes rf"\d+{suffix}", placing the r ahead of the f in the new prefix, so \d stays the literal text it was in the source template.

pattern = r"{}\d+".format(head) carries an r prefix. The rewrite writes rf"{head}\d+", keeping the r and adding only the f, so \d stays a literal backslash and a d rather than becoming an escape.

In the raw template r"\N{SNOWMAN} %s", \N{SNOWMAN} is literal text rather than a name escape, because an r prefix processes no escapes. The rewrite doubles the braces to \N{{SNOWMAN}} while %s becomes {name}, because a bare { would open a replacement field once the string is an f-string.

The parentheses around "%s" % (value,) group a binary expression, and the f-string that replaces it is a single atom needing no grouping. The rewrite widens the replaced span to take the parentheses with it, so the line is written as label = f"{value}" rather than leaving a redundant pair for a later rule to remove.

held carries a # prose: skip directive at the end of its line, and converted on the line below carries none. held keeps its "%s" % (value,) template and converted becomes f"{other}", because the directive reaches no further than its own line.

banner is a """ template whose body runs across two lines, with %s on the first. The rewrite keeps the """ delimiters and writes {name} in place of %s, leaving the second line untouched, and each physical line of the body is measured against the line budget on its own.

The argument tuple is written across four lines, with first and second each whole on a line of their own. The rewrite writes the one-line f"{first} and {second}", because each member is a single expression the field can carry and the tuple's layout goes with the tuple.

Each field writes an accessor after its argument reference, {0.attr} an attribute and {1[2]} a numeric index. The rewrite reattaches each accessor to the argument it read, so the fields become {record.attr} and {rows[2]}.

The template names its arguments out of order, {1} reading the second argument, first, and {0} the first, second. The rewrite resolves each index to the expression it binds, so swap becomes f"{first} before {second}" with the template's own ordering kept.

Each empty {} in "{}={}".format(key, value) reads the next positional argument, the first taking key and the second value, the numbering str.format applies at runtime. The rewrite moves them inline in that order, so label becomes f"{key}={value}".

{{literal}} in the .format template is already the escaped spelling of literal braces, and an f-string reads the doubled form the same way str.format does. The rewrite keeps {{literal}} untouched while {} becomes {value}, so the rendered text matches what the call produced.

flags uses the +, #, and blank-sign printf flags, and each carries into the matching format spec, so %+f becomes {delta:+f}, %#x keeps its alternate-form # in {mask:#x}, and % f becomes {offset: f}, where the blank sign carries over only when no explicit sign character supersedes it.

No Change

{key} is filled through the **table expansion, so no argument in the .format call names the value the field reads. The call stays as written, because the rewrite has no expression to put inside the field.

No Change

The right-hand side of % in out = "%s" % value is the bare name value rather than a tuple literal, so nothing in the source pins what it is at runtime. The line stays as written, because a one-element tuple in value is unpacked by % and rendered as its element, whereas f"{value}" would render the tuple itself.

No Change

The # the note stays comment sits inside noted's % construct, on the line that opens the argument tuple. The template stays as written, because an f-string has no place for a comment and converting would drop the note.

No Change

later = "%s" % (lambda: 1,) passes a lambda as the value. The template stays as written, because the lambda's : inside a replacement field would read as the start of a format spec and change what the field means.

No Change

The template's one spec reads kept, whereas the dict also carries "spare": ignored, an entry no spec names. The line stays as written, because the dict literal still evaluates ignored at runtime and a rewrite to f"{chosen}" would drop that evaluation with the entry.

No Change

Both %(v)s specs read the same mapping key, and the dict binds "v" to build(). The line stays as written, because the % form evaluates build() once while filling both specs whereas an f-string repeating the expression would call it twice.

No Change

"%.5x" renders bits with at least five hex digits, because a precision on %x sets a floor on the digit count. The line stays as written, because the format-spec mini-language accepts no precision on its integer x type, in that {:.5x} raises ValueError, so no equivalent spec exists.

No Change

Moving first_component_name, second_component_name, and third_component_name into one f-string would push summary's line past code-line-length. The % template and its argument tuple stay as written, because no layout rule reaches inside an f-string to wrap it back, whereas the tuple is a collection the layout rules can break.

No Change

*pair expands at runtime into however many arguments pair contains, so neither {} field maps to an expression visible in the source. The call stays as written, because the rewrite has no per-field value to move inline.

No Change

The field {0[key]} indexes the first argument with the literal string key, which a str.format field writes without quotes. The call stays as written, because the inline spelling table["key"] would put " quotes inside an f-string opened with the same ", a nesting the target-version = "3.10" config rules out.

No Change

The value table["key"] in entry = "%s" % (table["key"],) is written with " quotes. The line stays as written, because moving the value into an f-string delimited by the same " would let the first inner quote close the string early.

No Change

padded writes a width on %s and cut writes a precision on it. Both templates stay as written, because %-8s renders None where the field spelling {:8} raises, and %.3s cuts the rendered text where {:.3} measures the value itself, so the f-string form would change what each line computes.

No Change

The template reads x twice, and the call binds x to build(). The line stays as written, because "{x} {x}".format(x=build()) evaluates build() once whereas f"{build()} {build()}" would call it twice, so the rewrite would change behavior.

No Change

The template is two adjacent literals, "%s opening " and "and closing", joined by implicit concatenation. The line stays as written, because prefer-fstring never converts across an authored split, which is the layout stack-adjacent-strings writes.

No Change

count, index, and glyph use the %d, %i, and %c conversions, which accept values their f-string presentation types reject. All three lines stay as written, because %d and %i truncate a float where :d raises, %c renders a one-character string where :c raises, and nothing in the source pins the runtime types of total, n, or code.

  1. chr(i) sits ahead of '\\u{0:04x}'.format(i) on a row that fits the 50-column budget once the call becomes an f-string. reflow-calls measures the tail past chr(i) at the width of that f-string and passes over the ( of .format(, which the conversion removes, so chr(i) stays inline and the pass settles.

  2. At a 52-column budget, stacking the expression statement fetch(...).order_by(name).all() inside f sets the receiver fetch( on a row at indent 8, so the argument list reflow-calls explodes lands at indent 12, where the f-string prefer-fstring writes for "%s:%s" % (gamma.get(key).strip().lower(), delta) ends three columns past the budget. stack-method-chains measures the receiver's template from the indent the stack moves it to rather than the one the statement sits at, so the template stays as written and its three-link inner chain breaks under the count trigger in the same pass.

  3. At a 50-column budget, stacking fetch(...).order_by(name).all() inside a parenthesis pair sets the receiver fetch( on a row at indent 4, so the argument list reflow-calls explodes lands at indent 8, where the f-string prefer-fstring writes for "%s:%s" % (gamma.get(key).strip().lower(), delta) ends one column past the budget. stack-method-chains reads whether a template inside the receiver converts from the reflow-calls walk over the receiver where the stack lands it, so the template stays as written and its three-link inner chain breaks under the count trigger in the same pass.

  4. At a 36-column budget, stacking fetch(...).order_by(name).all() explodes fetch( at indent 8, and reflow-collections expands the operand tuple of "%s=%s" % (field.name.strip().lower(), value) at indent 12, where the 26-column field.name.strip().lower() runs two columns past the budget. stack-method-chains measures a chain inside the tuple from the row the tuple's expansion lands it on, so the chain breaks under the width trigger in the same pass rather than on the next.

  5. At a 39-column budget, advise( explodes, and the f-string prefer-fstring writes for "%s:%s" % (gamma.get(key).strip(), delta) reaches the budget exactly on the row its argument lands on. stack-method-chains leaves gamma.get(key).strip() joined inside a template reflow-calls leaves for prefer-fstring, so the chain lands in a replacement field and the template converts in the same pass.

  6. At a 50-column budget, stacking query.filter(...).order_by(name).all() sets query.filter( on a row at indent 4, so the argument list reflow-calls explodes lands at indent 8, where the f-string prefer-fstring writes for "%s:%s" % (gamma.get(key).strip().lower(), delta) ends one column past the budget. stack-method-chains reads whether a template inside a link converts from the reflow-calls walk over that link's arguments where the stack lands them, so the template stays as written and its three-link inner chain breaks under the count trigger in the same pass.

  7. At a 32-column budget, the f-string prefer-fstring writes for "%(name)s: %(value)s" % {...} runs past the budget, so reflow-collections expands the dict. reflow-collections reads the ( of describe( as a bracket a layout rule can break, since the rewrite around it reaches past the dict it lays out, so align-colons pads "name" and describe( explodes in the same pass.

  8. LS32 is built through str.format, a call band-constants pins where the source wrote it. The f-string prefer-fstring writes in its place reads HEX and IPV4, so it climbs into the derived sub-band below VARIATIONS, and SUBS, which reads LS32, follows it there. band-constants runs behind prefer-fstring, so the conversion and the banding settle in one pass, whereas the other order leaves band-constants a second move to make.

  9. "{}:{}".format(alpha, beta).order_by(name).first().all() carries four links as written, but prefer-fstring rewrites "{}:{}".format(alpha, beta) as the 17-column f"{alpha}:{beta}" in the same pass. stack-method-chains reads the chain as it stands once that rewrite lands, the f-string as its receiver and .format no longer a link, so the receiver runs wider than max-shift allows and the three remaining links take the full split, which a second pass leaves unchanged.

  10. args's list rejoins onto one row at the width its two f-strings take together, and each call alone would still leave that row past the 79-column budget. prefer-fstring measures each rewrite with every other rewrite on its row in place, so both calls convert in the pass that joins the list, where holding each call to the budget alone would leave the joined row too wide for either.

  11. At a 50-column budget, the value "%s:%s" % (type(exc), exc) runs "faultString"'s entry past the budget, whereas the f-string prefer-fstring writes for it fits. reflow-collections measures the value at the width of f"{type(exc)}:{exc}", so the entry stays on one row and a second pass finds nothing left to join.

  12. At a 71-column budget, the f-string prefer-fstring writes for "%s %s" % ([alpha_value, beta_value, gamma_value], describe(first)) reaches the budget exactly. reflow-collections leaves a literal on its row where the reflow-calls walk leaves the template around it for prefer-fstring, so the conversion is the one edit reported and the list stays joined inside the f-string.

  13. x's list carries bracket padding strip-stranded-padding removes, and with that padding gone and "%s" % ( alpha, ) converted, the row takes exactly the 19-column budget. prefer-fstring measures the row with the padding already cleared, as the layout rules forecast it, so the template converts in the pass that keeps the list on one row.

  14. type(exc) sits inside ( type(exc), exc ), the operand of a % template prefer-fstring rewrites as f"{type(exc)}:{exc}", and the tuple's padding falls inside the text that rewrite replaces. reflow-calls measures the template at the width of that f-string, whose row reaches the 73-column budget exactly, and leaves the calls inside it unwalked, so type(exc) stays on its row and the template converts in the same pass.

  15. ( type(exc), exc ) carries padding strip-stranded-padding removes, and the f-string prefer-fstring writes replaces that tuple whole. At a 46-column budget the one-row entry reads one column over with each space counted once, so the value hangs after :, where counting the padding twice would rejoin a row the conversion then leaves too wide to take.

  16. total is assigned a % template whose tuple arrives exploded, first and second each on their own line. reflow-collections condenses the tuple onto one line first, and prefer-fstring then measures the conversion against the budget and rewrites "%s and %s" % (first, second) to `f"{first} and {second}", each member inline in the template.

  17. prefer-fstring rewrites r"%s:%s" % ( type(exc), exc ) as rf"{type(exc)}:{exc}", an edit that starts past the r both strings share and so inside the string token. reflow-calls measures that edit across the whole token it starts in, and the row the f-string lands on reaches the 74-column budget exactly, so type(exc) stays on its row and the template converts in one pass.

  18. At an 80-column budget, args's comprehension written across two rows overflows one row while its element calls "{}={!r}".format(name, value), whereas the f-string fits. reflow-collections measures the call at the width of f"{name}={value!r}" and rejoins the comprehension in the same pass that converts it.

  19. At a 50-column budget, the f-string prefer-fstring writes for "%s -> %s" % (alias, name) fits its row once the comprehension's (alias, name) target breaks open. The reflow-calls walk reads the operand tuple that rewrite replaces as holding its row, so it measures the template through the target's ( and leaves it for prefer-fstring, and reflow-collections expands the target rather than the operand, which lets the template convert in the same pass.

  20. At a 38-column budget, "faultString"'s value wrap("%s" % (f(x),)) holds a template prefer-fstring rewrites as f"{f(x)}" inside the call, so the rewrite covers the argument rather than the whole value. reflow-collections hangs a value beneath its key only where a rewrite covers that value whole, so the entry stays beside "faultString": and wrap( explodes around the f-string, which a second pass leaves unchanged.

  21. With strip-stranded-padding off, the padding inside .format( type(exc), exc ) stays in the source and the row carrying it runs past the 73-column budget. reflow-calls measures the str.format() call at the width of the f-string prefer-fstring rewrites it as, whose row reaches the budget exactly, and leaves type(exc) unwalked, so type(exc) keeps its row and the call converts in one pass.

  22. With strip-stranded-padding off, the padding inside ( type(exc), exc ) stays in the source and the row carrying it runs past the 79-column budget. reflow-calls measures the % template at the width of the f-string prefer-fstring rewrites it as rather than at its padded text, and leaves type(exc) unwalked, so the call keeps its row and the template converts in one pass.

  23. At a 68-column budget, the f-string prefer-fstring writes for "%s:%s" % ( type(exc), exc ) still runs the row results.append( opens past the budget, so the call explodes and its dict lands on a row of its own. reflow-calls measures the template again from the column the dict lands at, where the f-string fits, and leaves type(exc) unwalked, so the template converts on that row in the same pass.

modernize-annotations reads target-version the same way, rewriting a legacy typing spelling wherever the runtime a project ships to supports the modern one.