29. String Formatting

Looking for an easy way to format variables for display, and mix them with other text? Add thousands separators (commas in the US) to a large number, and format decimals the way you want them? You need “string formatting.”

Python has three ways to format strings. The old string formatting which originally came from how older languages such as C did formatting. The newer way introduced in Python 3 that was supposed to be better, but arguably wasn’t. And the new way introduced in Python 3.6 with PEP-498 . We’ll cover the newest way.

29.1. Quick Reference

Here is a quick table for reference when doing text formatting. For a detailed explanation of how text formatting works, keep reading.

Example Formatting Commands
Number Format Output Description
x = 3.1415926 print(f"{x:.2f}") 3.14 2 decimal places
x = 3.1415926 print(f"{x:+.2f}") +3.14 2 decimal places with sign
x = -1 print(f"{x:+.2f}") -1.00 2 decimal places with sign
x = 3.1415926 print(f"{x:.0f}") 3 No decimal places (will round)
x = 5 print(f"{x:0>2d}") 05 Pad with zeros on the left
x = 1000000 print(f"{x:,}") 1,000,000 Number format with comma separator
x = 0.25 print(f"{x:.2%}") 25.00% Format percentage
x = 1000000000 print(f"{x:.2e}") 1.00e+09 Exponent notation
x = 11 print(f"{x:>10d}") ........11 Right aligned
x = 11 print(f"{x:<10d}") 11........ Left aligned
x = 11 print(f"{x:^10d}") ....11.... Center aligned

29.2. Decimal Numbers

Try running the following program, which prints out several random numbers.

Print an unformatted list of numbers
1
2
3
4
5
import random

for i in range(10):
    x = random.randrange(120)
    print("My number: ", x)

The output is left justified and numbers look terrible:

My number:  30
My number:  2
My number:  101
My number:  3
My number:  44
My number:  111
My number:  100
My number:  48
My number:  27
My number:  92

We can use string formatting to make the list of numbers look better by right-justifying them. The first step is to use the “Literal String Interpolation” on the string. See below:

import random

for i in range(10):
    x = random.randrange(120)
    print(f"My number: {x}")

This gets our program closer to right-justifying the number, but we aren’t quite there yet. See how the string starts with f?

The string will not print out the curly braces {} but instead replace them with the value in x. The output (below) looks just like what we had before.

The output:
My number: 23
My number: 92
My number: 102
My number: 19
My number: 85
My number: 114
My number: 37
My number: 101
My number: 35
My number: 18

To right justify, we add more information about how to format the number to the curly braces {}:

Right justified list of numbers
1
2
3
4
5
import random

for i in range(10):
    x = random.randrange(120)
    print(f"My number: {x:3}")
The output:
My number:  37
My number: 108
My number: 117
My number:  55
My number:  19
My number:  97
My number:  78
My number:  12
My number:  29
My number:   0

This is better; we have right justified numbers! But how does it work? The :3 that we added isn’t exactly intuitive. Looks like we just added a random emoji.

Here’s the breakdown: The { } tells the computer we are going to format a number. Inside we put the variable we want to format, x in this case. After the variable, we put a : to tell the computer we are about to give it formatting information.

In this case we give it a 3 to specify a field width of three characters. The field width value tells the computer to try to fit the number into a field three characters wide. By default, it will try to right-justify numbers and left-justify text.

Even better, the program no longer needs to call str( ) to convert the number to a string! Leave the string conversions out.

What if you had large numbers? Let’s make bigger random numbers:

Bigger numbers that are hard to read
1
2
3
4
5
import random

for i in range(10):
    x = random.randrange(100000)
    print(f"My number: {x:6}")

This gives output that is right justified, but still doesn’t look good.

The output:
My number:  89807
My number:   5177
My number:  24067
My number:  19887
My number:  54155
My number:  49288
My number:  31412
My number:  49633
My number:  43406
My number:  37398

Where are the commas? This list would look better with separators between each three digits. Take a look at the next example to see how they are added in:

Adding a thousands separator
1
2
3
4
5
import random

for i in range(10):
    x = random.randrange(100000)
    print(f"My number: {x:6,}")

The output:

My number: 86,631
My number: 57,165
My number: 19,835
My number: 22,560
My number: 43,161
My number: 16,604
My number: 20,544
My number: 33,906
My number: 89,846
My number: 27,350

We added a comma after the field width specifier, and now our numbers have commas. That comma must go after the field width specifier, not before. Commas are included in calculating the field width. For example, 1,024 has a field width of 5, not 4.

We can print multiple values, and combine the values with text. Run the code below.

Printing more than one variable at a time
1
2
3
4
x = 5
y = 66
z = 777
print(f"A - '{x}' B - '{y}' C - '{z}'")

The program will substitute numbers in for the curly braces, and still print out all of the other text in the string:

A - '5' B - '66' C - '777'

29.3. Strings

Let’s look at how to format strings.

The following list looks terrible.

Terrible looking list
1
2
3
4
5
my_fruit = ["Apples","Oranges","Grapes","Pears"]
my_calories = [4, 300, 70, 30]

for i in range(4):
    print(my_fruit[i], "are", my_calories[i], "calories.")

The output:

Apples are 4 calories.
Oranges are 300 calories.
Grapes are 70 calories.
Pears are 30 calories.

Now try it using the format command. Note how we can put additional text and more than one value into the same line.

Formatting a list of fruit
1
2
3
4
5
my_fruit = ["Apples", "Oranges", "Grapes", "Pears"]
my_calories = [4, 300, 70, 30]

for i in range(4):
    print(f"{my_fruit[i]:7} are {my_calories[i]:3} calories.")
The output:
Apples  are   4 calories.
Oranges are 300 calories.
Grapes  are  70 calories.
Pears   are  30 calories.

That’s pretty cool, and it looks the way we want it. But what if we didn’t want the numbers right justified, and the text left justified? We can use the < and > characters like the following example:

Specifying right/left alignment
1
2
3
4
5
my_fruit = ["Apples", "Oranges", "Grapes", "Pears"]
my_calories = [4, 300, 70, 30]

for i in range(4):
    print(f"{my_fruit[i]:>7} are {my_calories[i]:<3} calories.")
The output:
 Apples are 4   calories.
Oranges are 300 calories.
 Grapes are 70  calories.
  Pears are 30  calories.

29.4. Leading Zeros

This produces output that isn’t right:

Terrible looking clock
1
2
3
for hours in range(1,13):
    for minutes in range(0,60):
        print(f"Time {hours}:{minutes}")
The not-very-good output:
Time 8:56
Time 8:57
Time 8:58
Time 8:59
Time 9:0
Time 9:1
Time 9:2

We need to use leading zeros for displaying numbers in clocks. Rather than specify a 2 for the field width, instead use 02. This will pad the field with zeros rather than spaces.

Formatting time output with leading zeros
1
2
3
for hours in range(1, 13):
    for minutes in range(0, 60):
        print(f"Time {hours:02}:{minutes:02}")
The output:
Time 08:56
Time 08:57
Time 08:58
Time 08:59
Time 09:00
Time 09:01
Time 09:02

29.5. Floating Point Numbers

We can also control floating point output. Examine the following code and its output:

Formatting float point numbers
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
x = 0.1
y = 123.456789

print(f"{x:.1}  {y:.1}")
print(f"{x:.2}  {y:.2}")
print(f"{x:.3}  {y:.3}")
print(f"{x:.4}  {y:.4}")
print(f"{x:.5}  {y:.5}")
print(f"{x:.6}  {y:.6}")

print()
print(f"{x:.1f}  {y:.1f}")
print(f"{x:.2f}  {y:.2f}")
print(f"{x:.3f}  {y:.3f}")
print(f"{x:.4f}  {y:.4f}")
print(f"{x:.5f}  {y:.5f}")
print(f"{x:.6f}  {y:.6f}")
And here’s the output for that code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
0.1  1e+02
0.1  1.2e+02
0.1  1.23e+02
0.1  123.5
0.1  123.46
0.1  123.457

0.1  123.5
0.10  123.46
0.100  123.457
0.1000  123.4568
0.10000  123.45679
0.100000  123.456789

A format of .2 means to display the number with two digits of precision. Unfortunately this means if we display the number 123 which has three significant numbers rather than rounding it we get the number in scientific notation: 1.2e+02.

A format of .2f (note the f) means to display the number with two digits after the decimal point. So the number 1 would display as 1.00 and the number 1.5555 would display as 1.56.

A program can also specify a field width character:

Specifying a field width character
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
x = 0.1
y = 123.456789

print(f"My number: '{x:10.1}' and '{y:10.1}'")
print(f"My number: '{x:10.2}' and '{y:10.2}'")
print(f"My number: '{x:10.3}' and '{y:10.3}'")
print(f"My number: '{x:10.4}' and '{y:10.4}'")
print(f"My number: '{x:10.5}' and '{y:10.5}'")
print(f"My number: '{x:10.6}' and '{y:10.6}'")

print()
print(f"My number: '{x:10.1f}' and '{y:10.1f}'")
print(f"My number: '{x:10.2f}' and '{y:10.2f}'")
print(f"My number: '{x:10.3f}' and '{y:10.3f}'")
print(f"My number: '{x:10.4f}' and '{y:10.4f}'")
print(f"My number: '{x:10.5f}' and '{y:10.5f}'")
print(f"My number: '{x:10.6f}' and '{y:10.6f}'")

The format 10.2f does not mean 10 digits before the decimal and two after. It means a total field width of 10. So there will be 7 digits before the decimal, the decimal which counts as one more, and 2 digits after.

The output:
My number: '       0.1' and '     1e+02'
My number: '       0.1' and '   1.2e+02'
My number: '       0.1' and '  1.23e+02'
My number: '       0.1' and '     123.5'
My number: '       0.1' and '    123.46'
My number: '       0.1' and '   123.457'

My number: '       0.1' and '     123.5'
My number: '      0.10' and '    123.46'
My number: '     0.100' and '   123.457'
My number: '    0.1000' and '  123.4568'
My number: '   0.10000' and ' 123.45679'
My number: '  0.100000' and '123.456789'

29.6. Printing Dollars and Cents

If you want to print a floating point number for cost, you use an f. See below:

Specifying a field width character
1
2
3
4
5
6
7
8
cost1 = 3.07
tax1 = cost1 * 0.06
total1 = cost1 + tax1

print(f"Cost:  ${cost1:5.2f}")
print(f"Tax:    {tax1:5.2f}")
print(f"-------------")
print(f"Total: ${total1:5.2f}")

Remember! It would be easy to think that %5.2f would mean five digits, a decimal, followed by two digits. But it does not. It means a total field width of five, including the decimal and the two digits after. Here’s the output:

The output:
Cost:  $ 3.07
Tax:     0.18
-------------
Total: $ 3.25

Danger! The above code has a mistake that is very common when working with financial transactions. Can you spot it? Try spotting it with the expanded code example below:

Specifying a field width character
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
cost1 = 3.07
tax1 = cost1 * 0.06
total1 = cost1 + tax1

print(f"Cost:  ${cost1:5.2f}")
print(f"Tax:    {tax1:5.2f}")
print(f"-------------")
print(f"Total: ${total1:5.2f}")

cost2 = 5.07
tax2 = cost2 * 0.06
total2 = cost2 + tax2

print()
print(f"Cost:  ${cost2:5.2f}")
print(f"Tax:    {tax2:5.2f}")
print(f"-------------")
print(f"Total: ${total2:5.2f}")

print()
grand_total = total1 + total2
print(f"Grand total: ${grand_total:5.2f}")
The output:
Cost:  $ 3.07
Tax:     0.18
------------
Total: $ 3.25

Cost:  $ 5.07
Tax:     0.30
-------------
Total: $ 5.37

Grand total: $ 8.63

Spot the mistake? You have to watch out for rounding errors! Look at that example, it seems like the total should be $ 8.62 but it isn’t.

Print formatting doesn’t change the number, only what is output! If we changed the print formatting to include three digits after the decimal the reason for the error becomes more apparent:

The output:
Cost:  $3.070
Tax:    0.184
-------------
Total: $3.254

Cost:  $5.070
Tax:    0.304
-------------
Total: $5.374

Grand total: $8.628

Again, formatting for the display does not change the number. Use the round command to change the value and truly round. See below:

Specifying a field width character
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
cost1 = 3.07
tax1 = round(cost1 * 0.06, 2)
total1 = cost1 + tax1

print(f"Cost:  ${cost1:5.2f}")
print(f"Tax:    {tax1:5.2f}")
print(f"-------------")
print(f"Total: ${total1:5.2f}")

cost2 = 5.07
tax2 = round(cost2 * 0.06, 2)
total2 = cost2 + tax2

print()
print(f"Cost:  ${cost2:5.2f}")
print(f"Tax:    {tax2:5.2f}")
print(f"-------------")
print(f"Total: ${total2:5.2f}")

print()
grand_total = total1 + total2
print(f"Grand total: ${grand_total:5.2f}")
The output:
Cost:  $ 3.07
Tax:     0.18
-------------
Total: $ 3.25

Cost:  $ 5.07
Tax:     0.30
-------------
Total: $ 5.37

Grand total: $ 8.62

The round command controls how many digits after the decimal we round to. It returns the rounded value but does not change the original value. See below:

Specifying a field width character
1
2
3
4
5
6
x = 1234.5678
print(round(x, 2))
print(round(x, 1))
print(round(x, 0))
print(round(x, -1))
print(round(x, -2))

See below to figure out how feeding the round() function values like -2 for the digits after the decimal affects the output:

The output:
1234.57
1234.6
1235.0
1230.0
1200.0

29.7. Use in Arcade Programs

We don’t just have to format strings for print statements. The example timer.py uses string formatting to make an on-screen timer:

Code from timer.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def on_draw(self):
    """ Use this function to draw everything to the screen. """

    # Start the render. This must happen before any drawing
    # commands. We do NOT need an stop render command.
    arcade.start_render()

    # Calculate minutes
    minutes = int(self.total_time) // 60

    # Calculate seconds by using a modulus (remainder)
    seconds = int(self.total_time) % 60

    # Figure out our output
    output = f"Time: {minutes:02d}:{seconds:02d}"

    # Output the timer text.
    arcade.draw_text(output, 300, 300, arcade.color.BLACK, 30)

def update(self, delta_time):
    """
    All the logic to move, and the game logic goes here.
    """
    self.total_time += delta_time

You can also use it to display the score, or any other statistics you’d like to show the player.