String Formatting Deep Dive
You've used f-strings since the first lessons to drop values into text. They can do much more: round numbers, line up columns, add thousands separators, show percentages and pad with zeros. All of it uses a small format specification after a colon inside the braces.
The format specifier
Inside an f-string, {value:spec} formats the value according to spec:
price = 1234.5678
print(f"{price}")
print(f"{price:.2f}") # 2 decimal places
print(f"{price:,.2f}") # thousands separator tooThe pieces of a spec always appear in this order, and each one is optional:
{value:[fill][align][sign][width][,][.precision][type]}
That looks like a lot, but you'll rarely use more than two or three pieces at once. For example, {price:>12,.2f} breaks down like this:
| Piece | Here | Meaning |
|---|---|---|
> |
align | line the value up on the right |
12 |
width | take up at least 12 characters |
, |
grouping | put commas between the thousands |
.2 |
precision | 2 digits after the decimal point |
f |
type | show it as a fixed-point (decimal) number |
The result is 1,234.57. Formatting always produces text: it changes how the value is shown, never the value itself. price is still 1234.5678 afterwards.
Let's go through the pieces one at a time.
Decimal places
.Nf shows a number with exactly N digits after the decimal point, rounding as needed:
import math
print(f"{math.pi:.0f}")
print(f"{math.pi:.2f}")
print(f"{math.pi:.5f}")
# whole numbers get decimals too
print(f"{2:.2f}")f formats only work on numbers. f"{'abc':.2f}" is a ValueError, and so is a number stored as text, like "3.5"; convert it with float() first.
Thousands separators
A comma (or an underscore) groups digits in thousands:
population = 8_045_311_447
print(f"{population:,}")
print(f"{population:_}")
print(f"{1234567.891:,.2f}")Percentages
The % type multiplies by 100 and adds a percent sign:
ratio = 0.4567
print(f"{ratio:%}")
print(f"{ratio:.1%}")
print(f"{3 / 8:.0%}")Width and alignment
A number after the colon sets the minimum width. < aligns left, > aligns right, and ^ centres:
print(f"[{'left':<10}]")
print(f"[{'right':>10}]")
print(f"[{'centre':^10}]")
# numbers align right by default
print(f"[{42:>6}]")
# strings align left by default
print(f"[{'text':10}]")The square brackets are only there so you can see the spaces. The width is a minimum: 'centre' is 6 characters, so 4 spaces are added to make 10, two on each side. A value that's already longer than the width is never cut short; it just takes up more room.
Fill characters
Put a character before the alignment to fill the space with it instead of spaces:
print(f"{'Menu':*^20}")
print(f"{'Total':.<15}{9.5:.>8.2f}")
# pad with zeros
print(f"{7:0>3}")
print(f"{42:05}")In {'Menu':*^20}, the * is the fill character, ^ centres, and 20 is the width. To pad with a fill character, you must also give an alignment. {42:05} is a shortcut: a 0 right before the width pads numbers with zeros, so 42 becomes 00042. That's useful for things like invoice numbers.
Signs
Control whether positive numbers show a sign:
# + : always show + or -
for change in [3.5, -1.25, 0]:
print(f"{change:+.2f}")Without the +, only negative numbers get a sign. With it, 3.5 shows as +3.50, which is handy for changes such as price moves or score differences. Zero gets a + too.
Other number formats
n = 255
print(f"{n:b}") # binary
print(f"{n:o}") # octal
print(f"{n:x}") # hexadecimal
print(f"{n:X}")
print(f"{n:#x}") # with 0x prefix
print(f"{1234567:e}")
print(f"{0.000123:.2e}")Building a neat table
Widths and alignment really pay off when you print data in columns:
items = [("Coffee", 2, 3.5), ("Blueberry muffin", 1, 2.75), ("Juice", 3, 4.0)]
print(f"{'Item':<18}{'Qty':>4}{'Price':>9}")
print("-" * 31)
total = 0
for name, qty, price in items:
line = qty * price
total += line
print(f"{name:<18}{qty:>4}{line:>9.2f}")
print("-" * 31)
print(f"{'Total':<22}{total:>9.2f}")Every row uses the same widths, 18 + 4 + 9 = 31 characters, which is why the columns line up and the dashed lines are 31 long. Text columns are left-aligned (<) and number columns right-aligned (>), so the decimal points line up. The total row uses 22 for its label because it spans the item and quantity columns (18 + 4).
Expressions and debugging
Any expression works inside the braces, and adding = prints the expression as well as its value, which is great for debugging:
width = 7
height = 3
print(f"{width * height = }")
print(f"{width=}, {height=}")
# !r shows the value with quotes
print(f"{'python'.upper()!r}"){width * height = } prints width * height = 21: the expression exactly as you typed it (spaces included), then its value. It saves writing the label yourself when you're checking what your variables hold.
Dynamic widths
The spec itself can contain values in braces:
name = "Ada"
for w in [5, 8, 12]:
print(f"[{name:>{w}}]")
decimals = 3
print(f"{2 / 3:.{decimals}f}")Python fills in the inner braces first: with w = 8, {name:>{w}} becomes {name:>8}. That lets the width or the number of decimals come from a variable, for example the length of the longest name in a list.
Other formatting styles
You'll see two older styles in existing code. They use the same format specs:
name = "Ada"
score = 92.456
print("{} scored {:.1f}".format(name, score))
print("{n} scored {s:.1f}".format(n=name, s=score))
print("%s scored %.1f" % (name, score))
print(format(score, ".1f"))For new code, f-strings are the clearest choice.
Exercises
Exercise 1: Money
Print the salary with a currency sign, thousands separators and 2 decimal places: Salary: $52,750.50.
salary = 52750.5
, adds thousands separators and .2f gives two decimal places.
Combine them as {salary:,.2f}, with a $ just before the braces.
Exercise 2: Progress bar
Print the progress as a percentage with 1 decimal place, centred in a 20-character field filled with =. It should print =======62.5%========.
done = 25 total = 40
.1% shows a ratio as a percentage with one decimal place.
Put the fill and alignment in front: =^20 means fill with =, centred, 20 characters wide.
Exercise 3: Invoice numbers
Print each invoice number padded with zeros to 6 digits and prefixed with INV-, such as INV-000042.
invoice_ids = [42, 1337, 7]
06 pads a number with zeros to a width of 6.
Print f"INV-{invoice_id:06}" for each number.
Exercise 4: Leaderboard
Print the leaderboard as a table: the position right-aligned in 3 characters, the name left-aligned in 10, and the score right-aligned in 8 with thousands separators.
1 Leo 18,500
2 Mia 12,000
3 Ava 9,900
players = [("Leo", 18500), ("Mia", 12000), ("Ava", 9900)]
enumerate(players, start=1) gives the position, and you can unpack (name, score) in the same for line.
Use {position:>3}, {name:<10} and {score:>8,}.
Format specifiers at a glance
A quick reference for later. Results are shown with quotes, so you can see the spaces and padding.
| Spec | What it does | Example | Result |
|---|---|---|---|
.2f |
2 decimal places | f"{3.14159:.2f}" |
'3.14' |
.0f |
Rounds to a whole number | f"{2.718:.0f}" |
'3' |
, |
Thousands separators | f"{1234567:,}" |
'1,234,567' |
,.2f |
Separators and 2 decimal places | f"{1234.5:,.2f}" |
'1,234.50' |
_ |
Underscores as separators | f"{1234567:_}" |
'1_234_567' |
% |
Percentage | f"{0.256:.1%}" |
'25.6%' |
<6 |
Left-align in 6 characters | f"{'hi':<6}" |
'hi ' |
>6 |
Right-align in 6 characters | f"{'hi':>6}" |
' hi' |
^6 |
Centre in 6 characters | f"{'hi':^6}" |
' hi ' |
*^6 |
Centre in 6 characters, filling with * |
f"{'hi':*^6}" |
'**hi**' |
05 |
Pad a number with zeros to 5 characters | f"{42:05}" |
'00042' |
+ |
Always show the sign | f"{3.5:+.1f}" |
'+3.5' |
b, o, x |
Binary, octal, hexadecimal | f"{255:x}" |
'ff' |
#x |
Hexadecimal with the 0x prefix |
f"{255:#x}" |
'0xff' |
.2e |
Scientific notation | f"{1234567:.2e}" |
'1.23e+06' |
= |
Shows the expression and its value | f"{2 + 3 = }" |
'2 + 3 = 5' |
!r |
Shows the repr (strings get quotes) | f"{'hi'!r}" |
"'hi'" |
{w} inside a spec |
Takes the width from a variable | f"{'hi':>{4}}" |
' hi' |
Summary
{value:spec}formats a value inside an f-string..2fsets decimal places,,adds thousands separators, and%shows a percentage.- A width plus
<,>or^aligns text, and a fill character goes before the alignment. 05pads with zeros, and+always shows the sign.{expr=}prints the expression and its value, which is handy for debugging.str.format()and%are older styles with the same specs.
Know something worth teaching? Write your own tutorial with runnable examples and share it.