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 that would push its line past the budget keeps it too, because no layout rule reaches inside an f-string to wrap it back.

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. 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.

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