Why this matters
A table is easy to read when each column starts at the same place in every row. A table that does not line up makes the reader count characters to see which number belongs to which name. Programs that print reports, receipts, score lists and multiplication charts all need the columns to line up.
Python has several ways to print text with placeholders. The one this lesson uses is the f-string, which is a piece of text that starts with the letter f before its opening quote. Inside it, anything between braces is replaced by a value. A placeholder can also say how the value should look: how many characters wide it is, whether it is padded on the left or the right, and how many digits follow a decimal point. This lesson covers the width part of those placeholders. The earlier lesson on building rows as strings showed how to join pieces of text yourself. An f-string does the joining and the padding for you in one line.
Some terms first. A placeholder is the part in braces, such as {qty:>5d}. A format spec is the part after the colon inside a placeholder, such as >5d. An argument is a value that fills a placeholder. A width is the smallest number of characters a value takes up on screen. A column is a set of values in the same position across several rows.
Printing with a width
Start with one value and see how the width changes it. The program prints the same numbers four ways.
The output is:
[7]
[ 7]
[7 ]
[00007]
[42]
[ 42]
[42 ]
[00042]
[300]
[ 300]
[300 ]
[00300]
[ right]
[left ]
[ 7]Each part of {v:5d} has a job:
- The name before the colon,
v, is the value to print. 5is the width. The value is padded with spaces to at least 5 characters.dsays the value is a whole number. Numbers are written in decimal.
Look at the output. With no width, 7 takes one character. With {v:5d}, it takes five, and the spaces come before it. Numbers are right-aligned by default: the digits line up on the right edge, which is what makes a column of numbers easy to compare.
The < in {v:<5d} makes the value left-aligned, so the padding comes after the value. The 0 in {v:05d} pads with zeros instead of spaces. With text, a 0 adds zeros on the right, so {'ab':05} gives ab000.
The text lines show the other default. In Python, text is left-aligned by default, so {'left':<8} and {'left':8} give the same result. {'right':>8} pads the word on the left, so it is right-aligned. Names are read from left to right, so a column of names usually uses < with a width.
The last line uses the older % operator, as in "[%5d]" % 7. It takes the same width numbers. Newer code usually uses f-strings, because the value sits next to its placeholder and is easier to read.
Here is the rule that trips up many readers: the width is a minimum, not a maximum. If the value is longer than the width, Python prints all of it and does not cut anything off. The next example shows what that does to a table.
A multiplication table with one width
The next program prints a 12 by 12 multiplication table. The width is stored in one variable, so every column uses the same width.
The output is:
x 1 2 3 4 5 6 7 8 9 10 11 12
----------------------------------------------------
1 1 2 3 4 5 6 7 8 9 10 11 12
2 2 4 6 8 10 12 14 16 18 20 22 24
3 3 6 9 12 15 18 21 24 27 30 33 36
4 4 8 12 16 20 24 28 32 36 40 44 48
5 5 10 15 20 25 30 35 40 45 50 55 60
6 6 12 18 24 30 36 42 48 54 60 66 72
7 7 14 21 28 35 42 49 56 63 70 77 84
8 8 16 24 32 40 48 56 64 72 80 88 96
9 9 18 27 36 45 54 63 72 81 90 99 108
10 10 20 30 40 50 60 70 80 90 100 110 120
11 11 22 33 44 55 66 77 88 99 110 121 132
12 12 24 36 48 60 72 84 96 108 120 132 144The width can be put inside the braces as well. In {row:>{cell}}, the inner {cell} is replaced by the value of cell, which is 4, so the format spec becomes >4. Changing the one variable changes every column. This is the nested form of the same idea as the single-width placeholder above.
The end="" in print stops print from adding a line break after each cell, so the cells of one row stay on the same line. The last print() with nothing in it writes the line break at the end of the row.
The width of 4 was chosen from the data. The largest product is 144, which has 3 digits. The width must be at least 3. Using 4 gives one space between the columns, because the number is right-aligned and one character of each 4-character cell is free. The rule is: find the number of digits in the largest value, then add one for the space between columns.
The header row prints x in the top left corner and the numbers 1 to 12 across. The line under it is made with "-" * (...). Multiplying a string by a number repeats it, so "-" * 5 is five dashes. Its length is cell * (size + 1), which is the width of the row labels plus the 12 columns. Writing the length as a formula, and not as a fixed number of dashes, keeps the line the same length when you change size or cell.
The row labels use the same width, so the first column is also lined up.
Columns of text and numbers
Most real tables mix text and numbers. Names need to be left-aligned, and numbers need to be right-aligned with the same number of digits after the decimal point. The widths also depend on the data: the longest name decides how wide the name column must be.
The next program prints a short price list. It reads the names from a list of records, measures the longest name, and builds the widths from that measurement.
A record here is a small object that only holds values. Python has a short way to write one, the dataclass. The line @dataclass(frozen=True) above the class is a decorator, which is a line that changes the class under it. It asks Python to write the setup code for the class. frozen=True means the values cannot be changed after the object is made. The lines name: str and the others are type hints, which say what kind of value each field should hold. Python does not check them when the program runs, but they make the code easier to read.
The output is:
Item Qty Price
--------------------------
Notebook 3 45.50
Pen 12 2.25
Desk lamp 1 899.00
Tape 40 0.80The variable name_width starts at the length of the word Item, which is 4, so the header always fits. The loop then raises it to the longest name. The longest name here is Desk lamp, with 9 characters, so name_width ends as 9. The function len returns the number of characters in a string, and max returns the larger of two numbers.
The three columns use the same pattern:
{item.name:<{name_width}}for the name. Left-aligned, so the names start at the same place.{item.quantity:>5d}for the quantity. Right-aligned in 5 characters, so the numbers end at the same place.{item.price:>10.2f}for the price. Thefmeans a floating-point number, which is a number with a decimal point. The.2means two digits after the point. The total width is 10, so the decimal point is always in the same column.
The header line uses the same widths, with the header words in place of the values, so each word sits above its column.
The separator line is made of the same numbers: name_width + 1 + 5 + 1 + 10. The two 1s are the spaces between the columns. The separator is 26 characters long, which matches the width of each row. If you change a column width, the separator changes too.
The price uses .2 for the digits after the point. That means 45.5 is printed as 45.50. Python always writes a dot as the decimal point in an f-string, whatever the country or language setting of the computer. This lesson does not use locale settings, so the output is the same on every machine.
Mistakes
A number with the wrong type
The d in {x:d} expects a whole number. Giving it a decimal number or a text value stops the program with an error:
try:
print(f"{2.5:d}")
except ValueError as error:
print(type(error).__name__ + ":", error)
try:
print(f"{'abc':5d}")
except ValueError as error:
print(type(error).__name__ + ":", error)The output is:
ValueError: Unknown format code 'd' for object of type 'float'
ValueError: Unknown format code 'd' for object of type 'str'The try and except lines catch the error, so the program can print its name and message and keep going. type(error).__name__ is the name of the error's type, such as ValueError. Without them, Python stops and prints a traceback. The message names the code d and the type that does not fit it. The fix is to use f for decimal numbers, or to leave out the code and let Python print the value as it is.
Too few values
A format string with two placeholders needs two values. The .format method fills {} placeholders with the values in its brackets, and it gives this error when a value is missing:
try:
print("{} and {}".format(1))
except IndexError as error:
print(type(error).__name__ + ":", error)
try:
print("%d and %d" % (1,))
except TypeError as error:
print(type(error).__name__ + ":", error)The output is:
IndexError: Replacement index 1 out of range for positional args tuple
TypeError: not enough arguments for format stringPython builds the whole text before print runs, so the error appears before any part of the line is written. An f-string has its values written next to each placeholder, so this kind of error does not come up with f-strings.
A width that is too small
The width is a minimum. The next example shows the effect:
print(f"[{144:2d}]")
print(f"[{144:3d}]")The output is:
[144]
[144]The number 144 needs 3 characters. With {144:2d} it takes 3, because the width cannot cut it. The line is one character longer than planned, and in a table the next column shifts one place to the right on that row only. The fix is to choose the width from the largest value in the column, as the multiplication table did. When the widths come from data, as in the price list, you do not have to guess.
Left and right confused
Text is left-aligned by default and numbers are right-aligned by default. The < and > signs override that, and mixing them up gives the wrong side:
print(f"[{'ab':<6}][{'ab':>6}]")
print(f"[{'ab':6}][{42:6}]")The output is:
[ab ][ ab]
[ab ][ 42]The first line shows the explicit signs. The second line shows the defaults: the text has no sign and stays on the left, and the number has no sign and goes to the right. Write the sign anyway when a column matters, so a reader does not have to remember the default.
Building the width by hand
When a width is calculated from data, it is easy to forget that the header also needs the space. The price list starts name_width at the length of Item. If it started at 0 and every name were shorter than Item, the header would be wider than its column and push the other headers out of line.
Practice
Exercise 1: predict the output
What does this line print? Work it out before you run it.
print(f"[{3.14159:6.2f}]")Show the answer
It prints [ 3.14]. The number 3.14 has 4 characters, so the width of 6 adds two spaces on the left. The .2 rounds the value to two digits after the point.
Exercise 2: a table of squares and cubes
Print the numbers 1 to 10 with their squares and cubes. Give each column a width of 5 or 8, and put a header above the rows.
Show the answer
print(f"{'n':>5}{'square':>8}{'cube':>8}")
for n in range(1, 11):
print(f"{n:>5d}{n * n:>8d}{n ** 3:>8d}")The output is:
n square cube
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
6 36 216
7 49 343
8 64 512
9 81 729
10 100 1000The widths are 8 for the squares and cubes, which fits the value 1000 with room to spare. The header uses the same widths, so the words line up above the numbers. The ** operator raises a number to a power, so n ** 3 is n cubed.
Exercise 3: a smaller multiplication table
Change the multiplication table program so it prints the numbers 1 to 5 with a cell width of 3. Work out the width you need before you run the code. Explain why 3 is enough.
Show the answer
size = 5
cell = 3
print(f"{'x':>{cell}}", end="")
for col in range(1, size + 1):
print(f"{col:>{cell}}", end="")
print()
print("-" * (cell * (size + 1)))
for row in range(1, size + 1):
print(f"{row:>{cell}}", end="")
for col in range(1, size + 1):
print(f"{row * col:>{cell}}", end="")
print()The output is:
x 1 2 3 4 5
------------------
1 1 2 3 4 5
2 2 4 6 8 10
3 3 6 9 12 15
4 4 8 12 16 20
5 5 10 15 20 25The largest product is 25, which has 2 digits. A width of 3 is enough, because it is the digits plus one space.
Exercise 4: names left-aligned, lengths right-aligned
Given the names Ann, Bartholomew and Chi, print each name left-aligned in a column as wide as the longest name. After each name, print its length right-aligned in a width of 3.
Show the answer
names = ["Ann", "Bartholomew", "Chi"]
width = 0
for name in names:
width = max(width, len(name))
for name in names:
print(f"{name:<{width}} {len(name):>3d}")The output is:
Ann 3
Bartholomew 11
Chi 3The width is 11, the length of Bartholomew. Each name is padded to 11 characters, then a space, then the length in a width of 3.
Exercise 5: where the leading space comes from
The program prints [ 12.3] for print(f"[{12.345:5.1f}]"). Explain where each character comes from.
Show the answer
The value rounds to 12.3, which is 4 characters. The .1 keeps one digit after the point, and the width of 5 adds one space on the left. So the output is one space, then 12.3, inside the brackets.
Summary
- A width such as
{v:5d}or{name:<9}sets the smallest number of characters a value takes up. Numbers align right by default, and text aligns left by default.<forces left and>forces right. - The width is a minimum. A value longer than its width is printed in full and moves the columns after it.
- Build the width from the data when you can, with the longest value deciding the width, plus one space between columns.
- Use the same width variable for the header, the separator line and every row, so the columns stay in step.
- A decimal number needs
fwith a number of digits after the point, such as{price:10.2f}. Thedcode only takes whole numbers. - Read the error message for a bad format. It names the code or the placeholder that failed.