The f-strings in Python3.6

Info

Updates

  • 2024-04-19: This post is completely rewritten for better reference.

String formatting can be regarded as one of the most common activities in daily programming. We often need to output various strings and precisely control their format.

You may still see the usage of % to format strings in some outdated Python tutorials. However, the f-strings have become the optimal choice for formatting strings since Python3.6. The main advantages in my opinion are:

  1. Expressions can be embedded in the string literals conveniently
  2. More human-readable

The following simple comparison shows the advantages of f-strings in readability:

python

name = "Martin"
f"My name is {name}"
'My name is Martin'

python

name = "Martin"
"My name is %s" % name
'My name is Martin'

We implemented the same string output in two different ways.

Without any effort, we can find that the f-strings here are to output the name (this requires you to use meaningful variable names, such as name here). On the contrary, it is slightly unnatural to look at the % format string. We first saw it use %s as a placeholder, then we need to go to the right to find out which variable it refers to.

The example here is too short to show the real power of f-strings. Just trying to imagine that we are reading a long format string written in % syntax, we will need to look it back and forth to figure out what it means. Not just about reading the code, if you write a long format string by yourself, you have to check whether the order of the placeholders and variables is consistent, which is undoubtedly a burden. 😫

Note
The minimal version of Python which supports f-strings is Python3.6.

To create a f-string, we first add the leading f before "..." or '...'.

python

f"..."
# or f'...'

Then, we can use {expression} inside the quotes. Python will evaluate this expression for us and print its corresponding value.

If we want to get more control over the output, we can use the format_specifier, which contains various optional parts for different purposes.

From the perspective of grammar, the syntax rule of f-strings is

python

f"...{expression:format_specifier}..."

Now let’s figure out how to use f-strings in various scenarios

python

left, right = 3, 5
f"{left} + {right} = {left + right}"
'3 + 5 = 8'
Tip
Using {} as a placeholder naturally leads to a question: what if we want to display {} in the string? The answer is very simple, we just need to add another {}, i.e. {{}}

python

f"{{}}"   
'{}'

It is also worth mentioning that : and ! and \ are not allowed in expression

These characters rarely appear in expression though. One usage scenario of \ is to escape quotes. However, we could just use a different type of quote inside the expression. As for :, a use case would be defining a lambda function in expression1. In this situation, we only need to enclose the lambda expression within ():

python

# Note that we need to add () around the lambda expression
f"{(lambda x: x + 1)(3)}" 
'4'

In Python, objects generally have two types of string representations, corresponding to the __str__ and __repr__ methods, where the former is typically user-facing and more readable, while the latter is typically developer-facing and aids in debugging programs. The f-strings allowes us to specify which one to use.

python

{foo!s}     # it's equal to call str(foo) first
{foo!r}     # similarily, call repr(foo) first
{foo!a}     # similarily,call ascii(foo) first

The padding and alignment involve the following issues:

  • Which character should we use? Here, we use fill to represent that.
  • To what length should it be padded? Here, we use width to represent that.
  • How should it be aligned? Here, we use align to represent that.

The general format_specifier for padding and alignment is [[fill]align][width].

Note
In grammar specification, the [] indicates that this part is optional.

The various ways of alignment options:

python

<        # left-aligned 
>        # right-aligned
^        # centered
=        # only valid for numeric types, pad between sign and digits

For example

python

f"{-1:*^9}"  # set `fill` to *, and set width to 9
'***-1****'

python

f"{-1:*>9}"  # set `fill` to *, and set width to 9
'*******-1'

python

f"{-1:*<9}"  # set `fill` to *, and set width to 9
'-1*******'

python

f"{-1:*=9}"  # set `fill` to *, and set width to 9
'-*******1'

It decides how the sign should be represented and is only valid for numerical values.

python

+      # both positive and negative
-      # only negative (default)
space  # a leading space for positive and minus sign for negative

python

f"{1:+}"
'+1'

python

f"{-1:+}"
'-1'

python

assert f"{1}" == f"{1:-}"   # because it's the default behavior

We can use .nf to keep n decimal places for float-point numbers, which is somewhat similar to C/C++’s printf. For example

python

x = 1.23456
f"{x:.1f}"
'1.2'

python

x = 1.23456
f"{x:.3f}"
'1.235'

We can use z to handle negative zero (i.e. -0.) since Python3.11. According to the PEP2, programmers usually will suppress negative zero.

python

x = -0.0001
f"{x:.1f}"   # set the precision to 1, so it will round to -0.0
'-0.0'

python

x = -0.0001
f"{x:z.1f}"  # with z, we will get 0.0 rather than -0.0
'0.0'

In Python, we can use the built-in functions to get the representation in other bases including bin and oct. You may attempt to use {bin(a)} to get the binary representation output of a. However, the f-strings offers a more elegant way to handle this

python

b        # base 2
o        # base 8
d        # base 10
x        # base 16, low-case letters
X        # base 16, upper-case letters
Tip
Adding a # character will add the corresponding prefix (e.g. 0b)

python

f"{15:b}"      # represent 15 in base 2
'1111'

python

f"{15:#b}"     # represent 15 in base 2, use # to add prefix 0b
'0b1111'

python

f"{15:#X}"     # represent 15 in base 16, use # to add prefix 0X
'0XF'

Specify the thousands separators. Two options available34

  • _
  • ,

Both separators will make the long numbers more human-readable.

python

f"{123456789:,}"
'123,456,789'

python

f"{1234.56789:,}"
'1,234.56789'

python

f"{123456789:_}"
'123_456_789'

python

f"{1234.56789:_}"
'1_234.56789'

We can use {expression=} for self-documenting expression since Python3.85.

How could it be helpful? As a print debugger, I usually print values of variables to debug a Python program. It’s kind of annoying that every time I need to write the variable name twice.

python

f"left={left}, right={right}"
'left=3, right=5'

Now we can write

python

f"{left=}, {right=}"
'left=3, right=5'

When I started to learn Python a long time ago, most of the tutorials said we should use % to format strings. After str.format appeared, it became a better choice and outdated the % syntax. F-strings, released with Python3.6, has become the best practice now.🚀 It meets the zen of Python - There should be one– and preferably only one –obvious way to do it