Learning Goals
3 minBy the end of this lesson you can:
- Write an f-string that drops variables into a sentence.
- Round a number to two decimal places inside the f-string with
:.2f. - Pad text and numbers to a fixed width so a column lines up.
Warm-Up · The Stray Spaces
5 minTwo ways to print the same line. Look closely at the spacing, and predict what the third line does.
item = "Hot dog" price = 8.5 print("Item:", item, "costs $", price) print("Item: " + item + " costs $ " + str(price)) print("Item: " + item + " costs $ " + price)
Answer
Output
Item: Hot dog costs $8.5 Item: Hot dog costs $8.5 Traceback (most recent call last): TypeError: can only concatenate str (not "float") to str
The first quietly adds a space after $ that you did not write — print puts one between every comma. The second needs str() around the number. The third forgets it and crashes.
With commas you cannot control spacing; with + you must convert every number by hand. An f-string has neither problem.
New Concept · Write the Sentence, Leave Gaps
12 minLike filling in a form. The sentence is printed already, with blanks; you write a name in one blank and a price in another. The f before the quote tells Python the blanks are live.
item = "Hot dog" price = 8.5 qty = 3 print(f"Item: {item} costs $ {price}") print(f"{qty} sticks of {item} = $ {price * qty}")
Output
Item: Hot dog costs $8.5 3 sticks of Hot dog = $25.5
Three things to notice:
- The
fis essential. Without it you get the literal text{item}, braces and all. No error — just wrong output. - Numbers need no conversion. Python calls
str()for you inside the braces. - Braces can hold a calculation.
{price * qty}works, as does any expression.
Money needs two decimal places
$25.5 is not how money is written, and $7.700000000000001 is worse. Add :.2f inside the braces.
price = 8.5 total = 2.2 + 5.5 print(f"Bad: $ {price}") print(f"Good: $ {price:.2f}") print(f"Bad: $ {total}") print(f"Good: $ {total:.2f}")
Output
Bad: $8.5 Good: $8.50 Bad: $7.7 Good: $7.70
Read :.2f as "format as a decimal number with 2 places". It rounds for display only — the variable itself is untouched.
Widths line a column up
A number after the colon reserves that much room. Text pads to the left, numbers to the right, which is exactly what a price column needs.
print(f"{'Item':<12}{'Qty':>4}{'Price':>9}") print(f"{'Pizza':<12}{2:>4}{7.00:>9.2f}") print(f"{'Hot chocolate':<12}{10:>4}{25.00:>9.2f}")
Output
Item Qty Price Pizza 2 7.00 Hot chocolate 10 25.00
{name:<12}— left-aligned in 12 characters.{qty:>4}— right-aligned in 4.{price:>9.2f}— right-aligned in 9, with 2 decimals. Width first, then the format.
Why it matters. Every receipt, report and table you print needs this. Ragged columns are hard to read; two extra characters in the braces fix them.
Worked Example · A Tidy Order Summary
12 minThree orders, printed as a table that lines up whatever the names are.
# order_summary.py - one aligned table, built with f-strings orders = [ ["Pizza", 2, 3.50], ["Hot chocolate", 3, 2.50], ["Pancake Telur", 1, 4.20], ] header = f"{'Item':<20}{'Qty':>5}{'Unit':>9}{'Total':>10}" print(header) print("-" * len(header)) grand_total = 0 for item, qty, unit in orders: line_total = qty * unit grand_total = grand_total + line_total print(f"{item:<20}{qty:>5}{unit:>9.2f}{line_total:>10.2f}") print("-" * len(header)) print(f"{'GRAND TOTAL':<20}{'':>5}{'':>9}{grand_total:>10.2f}") print(f"\nThank you! Please come again.")
Output
Item Qty Unit Total ------------------------------------------- Pizza 2 3.50 7.00 Hot chocolate 3 2.50 7.50 Pancake Telur 1 4.20 4.20 ------------------------------------------- GRAND TOTAL 15.70 Thank you! Please come again.
Worth stealing from this:
- The header sets the widths. Every row uses the same numbers, so the columns cannot drift apart.
"-" * len(header)draws a rule exactly as wide as the table, whatever you change the widths to.{'':>5}prints an empty column to keep the total under the right heading.
A name longer than 20 characters will not be cut — it pushes the rest of that row right. If cutting matters, slice it first: item[:20], using lesson 1.
Try It Yourself
13 minStore your name, your age and your height in metres. Print one sentence using a single f-string, with the height shown to two decimal places.
Hint
name = "Mia" age = 13 height = 1.523 print(f"{name} is {age} and {height:.2f} m tall.")
Make a list of four days and what you spent each day. Print a table with a Day column 10 wide on the left and a right-aligned Spent column 8 wide showing two decimals.
Finish with a total row and a line of dashes above it. Then add one line printing the average per day, also to two decimals.
Mini-Challenge 🔥 · Debug: The Receipt That Would Not Line Up
8 minFelix's stall receipt prints, but the prices wander and one line shows a suspicious number. Find three mistakes.
# receipt.py - buggy
items = [["Iced Cocoa", 2, 3.00], ["Ice cream", 1, 4.90], ["Noodles", 3, 6.10]]
total = 0
for item, qty, price in items:
line = qty * price
total = total + line
print("{item:<12}{qty:>3}{line:>8.2f}")
print(f"TOTAL {total}")Answer
- The row string has no
f. It prints the literal text{item:<12}{qty:>3}{line:>8.2f}three times, braces and all. No error at all — the giveaway is that the output looks like the code. - The total has no format.
{total}gives26.599999999999998, because 6.10 cannot be stored exactly in binary. It needs{total:.2f}. - The total is not aligned with the column above it. Reusing the row widths puts it under the prices instead of floating after a space.
The working version:
# receipt.py - fixed items = [["Iced Cocoa", 2, 3.00], ["Ice cream", 1, 4.90], ["Noodles", 3, 6.10]] total = 0 for item, qty, price in items: line = qty * price total = total + line print(f"{item:<12}{qty:>3}{line:>8.2f}") print(f"{'TOTAL':<12}{'':>3}{total:>8.2f}")
Output
Iced Cocoa 2 6.00 Ice cream 1 4.90 Noodles 3 18.30 TOTAL 29.20
The missing f is the one to remember. It fails silently, and the output tells you immediately if you look at it.
Recap
3 minAn f-string is a normal string with an f in front, where anything in braces is worked out and dropped in. After a colon you can control the display: .2f for money, a number for width, < and > for alignment. Formatting changes what is printed, never the variable.
Vocabulary Card
- f-string
- A string prefixed with
fwhose{...}holes are filled with values. - format specifier
- The part after the colon, such as
:.2for:>8. - field width
- The minimum number of characters a value is padded to.
- floating-point
- How Python stores decimals — why totals sometimes end in stray digits.
Homework
4 minWrite report_card.py. Store five subjects and a mark out of 100 for each.
- Print a heading row, then one aligned row per subject.
- Show each mark as a percentage to one decimal place.
- Print the average to two decimal places under a line of dashes.
- Finish with one f-string sentence naming your best subject.
Every printed line must use an f-string — no commas in print, no +.
Sample · report_card.py
# report_card.py - an aligned report card, f-strings only subjects = [ ["Spanish", 78], ["English", 85], ["Mathematics", 92], ["Science", 88], ["Sejarah", 71], ] header = f"{'Subject':<16}{'Mark':>6}{'Percent':>10}" print(header) print("-" * len(header)) total = 0 best_name = subjects[0][0] best_mark = subjects[0][1] for name, mark in subjects: total = total + mark if mark > best_mark: best_mark = mark best_name = name print(f"{name:<16}{mark:>6}{mark:>9.1f}%") average = total / len(subjects) print("-" * len(header)) print(f"{'AVERAGE':<16}{'':>6}{average:>9.2f}%") print() print(f"Best subject: {best_name} with {best_mark} marks.")
Output
Subject Mark Percent -------------------------------- Spanish 78 78.0% English 85 85.0% Mathematics 92 92.0% Science 88 88.0% Sejarah 71 71.0% -------------------------------- AVERAGE 82.80% Best subject: Mathematics with 92 marks.
The percent column is width 9 plus the % sign, which makes 10 — matching the header. If your % pushed the column out by one, that is why.