1.4. Prefer Interpolated F-Strings Over C-style Format Strings and str.format

Strings are present throughout Python codebases. They’re used for rendering messages in user interfaces and command-line utilities. They’re used for writing data to files and sockets. They’re used for specifying what’s gone wrong in Exception details (see Item 27: “Use Comprehensions Instead of map and filter”). They’re used in debugging (see Item 80: “Consider Interactive Debugging with pdb” and Item 75: “Use repr Strings for Debugging Output”).

Formatting is the process of combining predefined text with data values into a single human-readable message that’s stored as a string. Python has four different ways of formatting strings that are built into the language and standard library. All but one of them, which is covered last in this item, have serious shortcomings that you should understand and avoid.

The most common way to format a string in Python is by using the % formatting operator. The predefined text template is provided on the left side of the operator in a format string. The values to insert into the template are provided as a single value or tuple of multiple values on the right side of the format operator. For example, here I use the % operator to convert difficult-to-read binary and hexadecimal values to integer strings:

>>> a = 0b10111011
>>> b = 0xc5f
>>> print('Binary is %d, hex is %d' % (a, b))
Binary is 187, hex is 3167

The format string uses format specifiers (like %d) as placeholders that will be replaced by values from the right side of the formatting expression. The syntax for format specifiers comes from C’s printf function, which has been inherited by Python (as well as by other programming languages). Python supports all the usual options you’d expect from printf, such as %s, %x, and %f format specifiers, as well as control over decimal places, padding, fill, and alignment. Many programmers who are new to Python start with C-style format strings because they’re familiar and simple to use.

There are four problems with C-style format strings in Python.

The first problem is that if you change the type or order of data values in the tuple on the right side of a formatting expression, you can get errors due to type conversion incompatibility. For example, this simple formatting expression works:

>>> key = 'my_var'
>>> value = 1.234
>>> formatted = '%-10s = %.2f' % (key, value)
>>> print(formatted)
my_var     = 1.23

But if you swap key and value, you get an exception at runtime:

reordered_tuple = '%-10s = %.2f' % (value, key) >>> Traceback ... TypeError: must be real number, not str

Similarly, leaving the right side parameters in the original order but changing the format string results in the same error:

reordered_string = '%.2f = %-10s' % (key, value) >>> Traceback ... TypeError: must be real number, not str

To avoid this gotcha, you need to constantly check that the two sides of the % operator are in sync; this process is error prone because it must be done manually for every change.

The second problem with C-style formatting expressions is that they become difficult to read when you need to make small modifications to values before formatting them into a string—and this is an extremely common need. Here, I list the contents of my kitchen pantry without making inline changes:

>>> pantry = [
>>>     ('avocados', 1.25),
>>>     ('bananas', 2.5),
>>>     ('cherries', 15),
>>> ]
>>> for i, (item, count) in enumerate(pantry):
>>>     print('#%d: %-10s = %.2f' % (i, item, count))
#0: avocados   = 1.25
#1: bananas    = 2.50
#2: cherries   = 15.00

Now, I make a few modifications to the values that I’m formatting to make the printed message more useful. This causes the tuple in the formatting expression to become so long that it needs to be split across multiple lines, which hurts readability:

>>> for i, (item, count) in enumerate(pantry):
>>>
>>>     print('#%d: %-10s = %d' % (
>>>         i + 1,
>>>         item.title(),
>>>         round(count)))
>>>
#1: Avocados   = 1
#2: Bananas    = 2
#3: Cherries   = 15

The third problem with formatting expressions is that if you want to use the same value in a format string multiple times, you have to repeat it in the right side tuple:

>>> template = '%s loves food. See %s cook.'
>>> name = 'Max'
>>> formatted = template % (name, name)
>>> print(formatted)
Max loves food. See Max cook.

This is especially annoying and error prone if you have to repeat small modifications to the values being formatted. For example, here I remembered to call the title() method multiple times, but I could have easily added the method call to one reference to name and not the other, which would cause mismatched output:

>>> name = 'brad'
>>> formatted = template % (name.title(), name.title())
>>> print(formatted)
Brad loves food. See Brad cook.

To help solve some of these problems, the % operator in Python has the ability to also do formatting with a dictionary instead of a tuple. The keys from the dictionary are matched with format specifiers with the corresponding name, such as %(key)s. Here, I use this functionality to change the order of values on the right side of the formatting expression with no effect on the output, thus solving problem #1 from above:

>>> key = 'my_var'
>>> value = 1.234
>>>
>>> old_way = '%-10s = %.2f' % (key, value)
>>>
>>> new_way = '%(key)-10s = %(value).2f' % {
>>>     'key': key, 'value': value} # Original
>>>
>>> reordered = '%(key)-10s = %(value).2f' % {
>>>     'value': value, 'key': key} # Swapped
>>>
>>> assert old_way == new_way == reordered

Using dictionaries in formatting expressions also solves problem #3 from above by allowing multiple format specifiers to reference the same value, thus making it unnecessary to supply that value more than once:

>>> name = 'Max'
>>>
>>>
>>> template = '%s loves food. See %s cook.'
>>> before = template % (name, name) # Tuple
>>>
>>>
>>> template = '%(name)s loves food. See %(name)s cook.'
>>> after = template % {'name': name} # Dictionary
>>>
>>>
>>> assert before == after

However, dictionary format strings introduce and exacerbate other issues. For problem #2 above, regarding small modifications to values before formatting them, formatting expressions become longer and more visually noisy because of the presence of the dictionary key and colon operator on the right side. Here, I render the same string with and without dictionaries to show this problem:

>>> for i, (item, count) in enumerate(pantry):
>>>     before = '#%d: %-10s = %d' % (
>>>         i + 1,
>>>         item.title(),
>>>         round(count))
>>>
>>>     after = '#%(loop)d: %(item)-10s = %(count)d' % {
>>>         'loop': i + 1,
>>>         'item': item.title(),
>>>         'count': round(count),
>>>     }
>>>
>>>     assert before == after

Using dictionaries in formatting expressions also increases verbosity, which is problem #4 with C-style formatting expressions in Python. Each key must be specified at least twice—once in the format specifier, once in the dictionary as a key, and potentially once more for the variable name that contains the dictionary value:

>>> soup = 'lentil'
>>> formatted = 'Today\'s soup is %(soup)s.' % {'soup': soup}
>>> print(formatted)
>>>
Today's soup is lentil.

Besides the duplicative characters, this redundancy causes formatting expressions that use dictionaries to be long. These expressions often must span multiple lines, with the format strings being concatenated across multiple lines and the dictionary assignments having one line per value to use in formatting:

>>> menu = {
>>>     'soup': 'lentil',
>>>     'oyster': 'kumamoto',
>>>     'special': 'schnitzel',
>>> }
>>> template = ('Today\'s soup is %(soup)s, '
>>>             'buy one get two %(oyster)s oysters, '
>>>             'and our special entrée is %(special)s.')
>>> formatted = template % menu
>>> print(formatted)
Today's soup is lentil, buy one get two kumamoto oysters, and our special entrée is schnitzel.

To understand what this formatting expression is going to produce, your eyes have to keep going back and forth between the lines of the format string and the lines of the dictionary. This disconnect makes it hard to spot bugs, and readability gets even worse if you need to make small modifications to any of the values before formatting.

There must be a better way.

The format Built-in and str.format Python 3 added support for advanced string formatting that is more expressive than the old C-style format strings that use the % operator. For individual Python values, this new functionality can be accessed through the format built-in function. For example, here I use some of the new options (, for thousands separators and ^ for centering) to format values:

>>> a = 1234.5678
>>> formatted = format(a, ',.2f')
>>> print(formatted)
>>>
>>> b = 'my string'
>>> formatted = format(b, '^20s')
>>> print('*', formatted, '*')
>>>
1,234.57
*      my string       *

You can use this functionality to format multiple values together by calling the new format method of the str type. Instead of using C-style format specifiers like %d, you can specify placeholders with {}. By default the placeholders in the format string are replaced by the corresponding positional arguments passed to the format method in the order in which they appear:

>>> key = 'my_var'
>>> value = 1.234
>>>
>>> formatted = '{} = {}'.format(key, value)
>>> print(formatted)
>>>
my_var = 1.234

Within each placeholder you can optionally provide a colon character followed by format specifiers to customize how values will be converted into strings (see help(‘FORMATTING’) for the full range of options):

>>> formatted = '{:<10} = {:.2f}'.format(key, value)
>>> print(formatted)
my_var     = 1.23

The way to think about how this works is that the format specifiers will be passed to the format built-in function along with the value (format(value, ‘.2f’) in the example above). The result of that function call is what replaces the placeholder in the overall formatted string. The formatting behavior can be customized per class using the format special method.

With C-style format strings, you need to escape the % character (by doubling it) so it’s not interpreted as a placeholder accidentally. With the str.format method you need to similarly escape braces:

>>> print('%.2f%%' % 12.5)
>>> print('{} replaces {{}}'.format(1.23))
>>>
12.50%
1.23 replaces {}

Within the braces you may also specify the positional index of an argument passed to the format method to use for replacing the placeholder. This allows the format string to be updated to reorder the output without requiring you to also change the right side of the formatting expression, thus addressing problem #1 from above:

>>> formatted = '{1} = {0}'.format(key, value)
>>> print(formatted)
>>>
1.234 = my_var

The same positional index may also be referenced multiple times in the format string without the need to pass the value to the format method more than once, which solves problem #3 from above:

>>> formatted = '{0} loves food. See {0} cook.'.format(name)
>>> print(formatted)
>>>
Max loves food. See Max cook.

Unfortunately, the new format method does nothing to address problem #2 from above, leaving your code difficult to read when you need to make small modifications to values before formatting them. There’s little difference in readability between the old and new options, which are similarly noisy:

>>> for i, (item, count) in enumerate(pantry):
>>>     old_style = '#%d: %-10s = %d' % (
>>>         i + 1,
>>>         item.title(),
>>>         round(count))
>>>     new_style = '#{}: {:<10s} = {}'.format(
>>>         i + 1,
>>>         item.title(),
>>>         round(count))
>>>
>>>     assert old_style == new_style

There are even more advanced options for the specifiers used with the str.format method, such as using combinations of dictionary keys and list indexes in placeholders, and coercing values to Unicode and repr strings:

>>> formatted = 'First letter is {menu[oyster][0]!r}'.format(
>>>     menu=menu)
>>> print(formatted)
>>>
First letter is 'k'

But these features don’t help reduce the redundancy of repeated keys from problem #4 above. For example, here I compare the verbosity of using dictionaries in C-style formatting expressions to the new style of passing keyword arguments to the format method:

>>> old_template = (
>>>     'Today\'s soup is %(soup)s, '
>>>     'buy one get two %(oyster)s oysters, '
>>>     'and our special entrée is %(special)s.')
>>> old_formatted = template % {
>>>     'soup': 'lentil',
>>>     'oyster': 'kumamoto',
>>>     'special': 'schnitzel',
>>> }
>>>
>>> new_template = (
>>>     'Today\'s soup is {soup}, '
>>>     'buy one get two {oyster} oysters, '
>>>     'and our special entrée is {special}.')
>>> new_formatted = new_template.format(
>>>     soup='lentil',
>>>     oyster='kumamoto',
>>>     special='schnitzel',
>>> )
>>>
>>> assert old_formatted == new_formatted

This style is slightly less noisy because it eliminates some quotes in the dictionary and a few characters in the format specifiers, but it’s hardly compelling. Further, the advanced features of using dictionary keys and indexes within placeholders only provides a tiny subset of Python’s expression functionality. This lack of expressiveness is so limiting that it undermines the value of the format method from str overall.

Given these shortcomings and the problems from C-style formatting expressions that remain (problems #2 and #4 from above), I suggest that you avoid the str.format method in general. It’s important to know about the new mini language used in format specifiers (everything after the colon) and how to use the format built-in function. But the rest of the str.format method should be treated as a historical artifact to help you understand how Python’s new f-strings work and why they’re so great.

1.4.1. Interpolated Format Strings

Python 3.6 added interpolated format strings—f-strings for short—to solve these issues once and for all. This new language syntax requires you to prefix format strings with an f character, which is similar to how byte strings are prefixed with a b character and raw (unescaped) strings are prefixed with an r character.

F-strings take the expressiveness of format strings to the extreme, solving problem #4 from above by completely eliminating the redundancy of providing keys and values to be formatted. They achieve this pithiness by allowing you to reference all names in the current Python scope as part of a formatting expression:

>>> key = 'my_var'
>>> value = 1.234
>>>
>>> formatted = f'{key} = {value}'
>>> print(formatted)
>>>
my_var = 1.234

All of the same options from the new format built-in mini language are available after the colon in the placeholders within an f-string, as is the ability to coerce values to Unicode and repr strings similar to the str.format method:

>>> formatted = f'{key!r:<10} = {value:.2f}'
>>> print(formatted)
'my_var'   = 1.23

Formatting with f-strings is shorter than using C-style format strings with the % operator and the str.format method in all cases. Here, I show all these options together in order of shortest to longest, and line up the left side of the assignment so you can easily compare them:

>>> f_string = f'{key:<10} = {value:.2f}'
>>>
>>> c_tuple  = '%-10s = %.2f' % (key, value)
>>>
>>> str_args = '{:<10} = {:.2f}'.format(key, value)
>>>
>>> str_kw   = '{key:<10} = {value:.2f}'.format(key=key,
>>>                                           value=value)
>>>
>>> c_dict   = '%(key)-10s = %(value).2f' % {'key': key,
>>>                                        'value': value}
>>>
>>> assert c_tuple == c_dict == f_string
>>> assert str_args == str_kw == f_string

F-strings also enable you to put a full Python expression within the placeholder braces, solving problem #2 from above by allowing small modifications to the values being formatted with concise syntax. What took multiple lines with C-style formatting and the str.format method now easily fits on a single line:

>>> for i, (item, count) in enumerate(pantry):
>>>     old_style = '#%d: %-10s = %d' % (
>>>         i + 1,
>>>         item.title(),
>>>         round(count))
>>>
>>>     new_style = '#{}: {:<10s} = {}'.format(
>>>         i + 1,
>>>         item.title(),
>>>         round(count))
>>>
>>>     f_string = f'#{i+1}: {item.title():<10s} = {round(count)}'
>>>
>>>     assert old_style == new_style == f_string

Or, if it’s clearer, you can split an f-string over multiple lines by relying on adjacent-string concatenation (similar to C). Even though this is longer than the single-line version, it’s still much clearer than any of the other multiline approaches:

>>> for i, (item, count) in enumerate(pantry):
>>>     print(f'#{i+1}: '
>>>
>>>           f'{item.title():<10s} = '
>>>           f'{round(count)}')
>>>
#1: Avocados   = 1
#2: Bananas    = 2
#3: Cherries   = 15

Python expressions may also appear within the format specifier options. For example, here I parameterize the number of digits to print by using a variable instead of hard-coding it in the format string:

>>> places = 3
>>> number = 1.23456
>>> print(f'My number is {number:.{places}f}')
>>>
My number is 1.235

The combination of expressiveness, terseness, and clarity provided by f-strings makes them the best built-in option for Python programmers. Any time you find yourself needing to format values into strings, choose f-strings over the alternatives.

1.4.2. Things to Remember

✦ C-style format strings that use the % operator suffer from a variety of gotchas and verbosity problems.

✦ The str.format method introduces some useful concepts in its formatting specifiers mini language, but it otherwise repeats the mistakes of C-style format strings and should be avoided.

✦ F-strings are a new syntax for formatting values into strings that solves the biggest problems with C-style format strings.

✦ F-strings are succinct yet powerful because they allow for arbitrary Python expressions to be directly embedded within format specifiers.