Skip to content

prefer-fstring

Old %-formatting and str.format() read at a remove from the values they splice, because the placeholders sit in the template whereas the expressions trail behind in a separate tuple or argument list. Counting positional slots back to the tuple is work the reader does on every line. An f-string sets each expression inline where it renders, so a substitution is read in place.

applies both conversions behind a facet apiece, rewrite-percent reaching the % operator and rewrite-str-format reaching the method call.

Both facets read target-version and both hold until it names Python 3.6 or higher, the release f-strings landed in. A project that has set no target-version at all holds every template.

Each %s reads the tuple member at its position, so every value moves inline where it renders.

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

What Each Facet Reaches

rewrite-percent reads the template through its specs 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.

A parenthesized key names the dict entry it reads, and the rewrite splices that entry's value into the field.

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

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 carries any attribute or index the field name spelled onto the value inline.

The accessor the field name spelled moves onto the value it accessed.

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

A conversion and a format spec both pass through unchanged, in that an f-string field reads the same grammar the template did, and the printf flags translate to their format-spec counterparts so - reads as <.

Where a Template Holds

The rewrite lands only where both forms render the same text, so several shapes stay as written.

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

An argument no field reads holds the whole call, because dropping it would drop its evaluation, and an argument two fields read holds whenever evaluating it runs code, since the call evaluates it once where the fields would twice.

The call evaluates build() once where the two fields would call it twice.

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

A value the field itself cannot carry holds too, covering a quote that would close the delimiter the f-string opened with, a backslash, a line break, and a brace. Those bounds are the ones every Python version accepts, so an emitted f-string parses below the PEP 701 floor as readily as above it. A comment anywhere inside the template or the call holds it as well, since an f-string has no place for one.

Configuration

KeyTypeDefaultMeaning
enabledbooltrueToggles 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 holds them both.

The Canonical Case

Each %s reads the tuple member at its position, so every value moves inline where it renders.

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

More Examples

Each becomes the !r or !a conversion an f-string field already carries.

A generator standing as the call's only argument carries no parentheses of its own, so the field supplies them.

Both trail the value unchanged, in that an f-string field reads the same grammar the template did.

%% collapses to the single % an f-string renders verbatim, leaving the precision on the field beside it.

The brace the template wrote literally becomes {{, so the f-string renders it rather than opening a field on it.

No literal hides a one-element tuple for % to unpack, so a lone spec reads it directly.

Each field carries the value its keyword named, leaving the call behind.

The printf flags translate to their format-spec counterparts, - reading as < and a zero pad surviving wherever no left adjust supersedes it.

The template parses under its raw reading, so no backslash in the body is taken for an escape.

The raw marker leads the f, leaving every backslash in the body unread as it was.

A raw template processes no escape, so \N{...} reads as literal text and its braces double like any other.

The f-string leaves the pair around it redundant, so the replaced span takes the pair in rather than leaving it for a later pass.

The directive holds the line it sits on, leaving the neighbour below it to convert.

The rewrite reuses the quotes the source wrote, so a multi-line body carries across unchanged and each of its lines answers to the budget on its own.

The members each sit on one line, so every one reads as a field and the call's own layout falls away with it.

The accessor the field name spelled moves onto the value it accessed.

Explicit indices read the arguments in whatever order the template states them.

An empty field takes the next positional argument, the same numbering str.format applies.

The escaped braces stay escaped, so the rendered text matches what the call produced.

A parenthesized key names the dict entry it reads, and the rewrite splices that entry's value into the field.

The +, #, and blank-sign flags each carry into the format spec, the blank sign surviving only where no sign character supersedes it.

No Change

value may hold a one-element tuple, which % unpacks and a replacement field does not, so the two forms would render differently.

No Change

An f-string has no place for a comment, so a template carrying one is left as written rather than converted without it.

No Change

The expansion names no argument a field could read in its place.

No Change

The dict evaluates build() once where the two fields would call it twice.

No Change

The field would read the lambda's colon as the start of its format spec, so the template is left as written.

No Change

A precision on %x sets a floor on the digit count where {:.5x} cuts the rendered text instead.

No Change

The call evaluates build() once where the two fields would call it twice.

No Change

No layout rule reaches inside an f-string, so a conversion that would push its line past code-line-length is left as the wrapped form the layout rules can still reach.

No Change

The expansion names no value a field could read in its place.

No Change

Writing table["key"] inline would reuse the delimiter the f-string opened with.

No Change

The inner quote would close the delimiter the f-string opened with.

No Change

A width on %s renders None where {:8} raises, and a precision cuts the rendered text where {:.3} measures the value itself.

No Change

The run stays the one authored shape

lays out.

No Change

%d truncates a float and %c maps an ordinal, where the matching presentation type raises instead.

No Change

Dropping the entry no spec names would drop its evaluation along with it.

reads target-version on the same axis, rewriting a legacy typing spelling wherever the runtime a project ships to carries the modern one.