Strings and text · about 40 min

Formatting with f-strings

Why this matters

Output is how your program communicates: reports, receipts, log lines, tables, error messages. Gluing strings with + and str() quickly becomes unreadable, and print with commas gives you no control over spacing or decimal places. Python's f-strings let you write the output's shape directly, with values dropped into place and formatted precisely: prices with two decimals, columns that line up, big numbers with separators. They are also one of the best debugging tools in the language.

The basic idea

Put an f before the opening quote, and any expression inside { } is evaluated and inserted:

name = "Ada"
age = 36
print(f"{name} is {age} years old.")
print(f"Next year {name} will be {age + 1}.")
print(f"Shouting: {name.upper()}!")
Ada is 36 years old.
Next year Ada will be 37.
Shouting: ADA!

Compare the alternatives:

print(name + " is " + str(age) + " years old.")
print(name, "is", age, "years old.")

The f-string is easier to read, converts values to text automatically, and gives you full control over spaces.

What happens underneath

An f-string is not a string with a special runtime meaning; it is compiled into code. For f"{name} is {age}", the bytecode compiler emits instructions that evaluate name, convert it with format(), evaluate age, convert it, and concatenate the pieces with the literal text in one efficient step (BUILD_STRING). You can check with dis.dis('f"{name} is {age}"'). Two consequences:

To include a literal brace, double it: f"{{x}} is {x}" gives {x} is 5 when x is 5.

Format specifications

After the expression, a colon introduces a format specification (format spec) that controls how the value is rendered:

 {value:spec}
        |
        [[fill]align][sign][0][width][,][.precision][type]

You rarely need all parts at once. Here are the ones you will use constantly.

Precision and type for numbers

pi = 3.14159265
print(f"{pi:.2f}")
print(f"{pi:.0f}")
print(f"{2.5:.3f}")
print(f"{1234.5:e}")
print(f"{0.256:.1%}")
3.14
3
2.500
1.234500e+03
25.6%

Note: Rounding uses the exact binary value of the float, so results may surprise you at the halfway point: f"{2.675:.2f}" gives 2.67, because 2.675 is actually stored as 2.67499999... (recall the float lesson). For money that must round exactly, use the decimal module.

Width and alignment

A width sets the minimum number of characters; shorter values are padded. Alignment chooses where the padding goes: < left, > right, ^ center. By default strings align left and numbers align right.

print(f"[{'cat':10}]")
print(f"[{'cat':>10}]")
print(f"[{'cat':^10}]")
print(f"[{42:10}]")
print(f"[{42:<10}]")
[cat       ]
[       cat]
[   cat    ]
[        42]
[42        ]

A fill character goes before the alignment symbol:

print(f"{'title':*^15}")
print(f"{7:0>3}")
print(f"{7:03}")
*****title*****
007
007

A leading 0 before the width is a shortcut for zero padding numbers (it also keeps the sign in front: f"{-7:04}" is -007).

Width is a minimum: a value longer than the width is never cut off, it just pushes the columns out of line.

Thousands separators and signs

n = 1234567.891
print(f"{n:,.2f}")
print(f"{n:_.0f}")
print(f"{5:+}")
print(f"{-5:+}")
1,234,567.89
1_234_568
+5
-5

, or _ groups digits by thousands; + always shows the sign.

Specs can use expressions too

The width itself can come from a variable, using nested braces:

width = 8
print(f"[{'hi':>{width}}]")
[      hi]

Building aligned tables

Combining widths and alignment produces tidy columns:

print(f"{'item':<10}{'qty':>5}{'price':>9}")
print(f"{'apple':<10}{3:>5}{0.5:>9.2f}")
print(f"{'watermelon':<10}{1:>5}{4.25:>9.2f}")
item        qty    price
apple         3     0.50
watermelon    1     4.25

The header uses the same widths as the rows, which is what makes it line up.

Conversions: !r and !s

Before formatting, you can convert the value: !s uses str() (the default), !r uses repr(). repr is the programmer's view: strings get quotes and invisible characters become escapes.

line = "ada \n"
print(f"value: {line}|")
print(f"value: {line!r}|")
value: ada
|
value: 'ada \n'|

The second line reveals the trailing space and newline at once.

The = debugging form

Adding = after an expression prints the expression's text, an equals sign, and its repr. It is the fastest way to inspect values while debugging:

x = 7
name = "ada"
print(f"{x=}")
print(f"{x * 2 = }")
print(f"{name=}")
print(f"{x=:05}")
x=7
x * 2 = 14
name='ada'
x=00007

Spaces around = are kept in the output. Strings show their quotes because = uses repr unless you give a format spec.

Older styles you will meet

Older code uses two other formatting styles. You should recognize them:

print("{} is {} years old".format("Ada", 36))
print("%s is %d years old" % ("Ada", 36))

str.format uses the same format-spec mini-language as f-strings ("{:.2f}".format(pi)), and is still useful when the template is stored separately from the values. The % style comes from C's printf and survives in some libraries, notably logging. For new code, prefer f-strings.

Common mistakes

Recap

Going further: read "Format Specification Mini-Language" in the Python library docs (under string). Then try formatting the current date: import datetime; print(f"{datetime.date.today():%A %d %B %Y}"). Objects can define their own format spec language, and dates do.

Exercises

Exercise 1 · write code

A receipt line

Read three lines: an item name, a quantity (whole number) and a unit price (decimal number). Print one line using an f-string:

3 x coffee @ 2.50 = 7.50

That is: quantity, x, the name, @, the unit price with exactly 2 decimals, =, the total (quantity times price) with exactly 2 decimals. The example is for inputs coffee, 3, 2.5.

File: main.py

Starting point

name = input()
qty = int(input())
price = float(input())
print(qty, "x", name, "@", price, "=", qty * price)

Example

input

coffee
3
2.5

expected output

3 x coffee @ 2.50 = 7.50

Example

input

bagel
7
1.333

expected output

7 x bagel @ 1.33 = 9.33
Hint 1

Inside an f-string, {price:.2f} formats a number with 2 digits after the point.

Hint 2

Any expression can go in the braces, including qty * price.

Reference solution
name = input()
qty = int(input())
price = float(input())
print(f"{qty} x {name} @ {price:.2f} = {qty * price:.2f}")

Exercise 2 · quiz

Read the format spec

What does this print? Each line is shown between | characters so that spaces are visible. Pick the correct output.

print(f"|{'ab':>5}|{7:03}|{3.14159:.1f}|")
Answer
  • | ab|007|3.1|

>5 right-aligns in a field 5 characters wide, so ab gets three spaces in front. 03 means width 3, padded with zeros: 007. .1f is fixed-point with one decimal: 3.1. Format specs follow the colon and are read as fill, alignment, sign, width, grouping, precision and type, in that order.

Exercise 3 · write code

Align a small table

Read three lines, each of the form name,score (a name without commas, and a whole-number score). Print a header line and the three rows in aligned columns:

For input lines ada,95, grace,100, linus,7 the output must be exactly:

name      |score
ada       |   95
grace     |  100
linus     |    7

(Write the header with the same format so it lines up.) Since loops are covered in the next module, you may simply repeat the three lines of code for each row.

File: main.py

Starting point

line = input()
print(line)

Example

input

ada,95
grace,100
linus,7

expected output

name      |score
ada       |   95
grace     |  100
linus     |    7
Hint 1

{name:<10} left-aligns in 10 characters; {score:>5} right-aligns in 5.

Hint 2

parts = line.split(",") gives the name in parts[0] and the score in parts[1].

Hint 3

The header needs the same widths: f"{'name':<10}|{'score':>5}".

Reference solution
print(f"{'name':<10}|{'score':>5}")
name, score = input().split(",")
print(f"{name:<10}|{int(score):>5}")
name, score = input().split(",")
print(f"{name:<10}|{int(score):>5}")
name, score = input().split(",")
print(f"{name:<10}|{int(score):>5}")

Exercise 4 · write code

Readable numbers

Read a whole number of bytes (possibly very large) and print three lines:

  1. bytes: then the number with commas as thousands separators
  2. MiB: then the number divided by 1048576 (that is, 1024 * 1024), with exactly 1 decimal
  3. debug: then the result of the f-string {n=} debugging form, where n is the name of your variable holding the number

For input 1234567890:

bytes: 1,234,567,890
MiB: 1177.4
debug: n=1234567890

Your variable must be named n for line 3 to match.

File: main.py

Starting point

n = int(input())
print("bytes:", n)

Example

input

1234567890

expected output

bytes: 1,234,567,890
MiB: 1177.4
debug: n=1234567890

Example

input

512

expected output

bytes: 512
MiB: 0.0
debug: n=512
Hint 1

A comma in the format spec groups thousands: {n:,}.

Hint 2

Combine grouping and precision if you like, but here only .1f is needed for line 2.

Hint 3

f"{n=}" prints the expression text, an equals sign, and the value.

Reference solution
n = int(input())
print(f"bytes: {n:,}")
print(f"MiB: {n / 1048576:.1f}")
print(f"debug: {n=}")

Exercise 5 · quiz

Why !r?

A user typed their name and your program prints f"Hello {name}!", which looks like Hello ada!, yet a comparison name == "ada" is False. Which f-string would best help you see what is in name?

Answer
  • f"{name!r}"

!r formats the value with repr(), which shows quotes and escapes invisible characters, so a trailing newline or space appears as 'ada\n' or 'ada '. ({len(name)=} would also hint that something is off, by printing len(name)=4, but !r shows you exactly what the extra character is.) The usual fix is name = input().strip().