Learning Goals
3 minBy the end of this lesson you can:
- Force a fixed number of decimal places with
{value:.2f}. - Set a minimum column width with
{value:10}. - Align with
<(left),>(right) and^(centre). - Pad numbers with leading zeros —
{n:03}— for IDs like005. - Add a thousands separator with
{n:,}.
Warm-Up
5 minYesterday's receipt printed RM 8.0. Compare with this version. Predict the output:
subtotal = 19.5 tax = 1.17 grand = 20.67 print(f"Subtotal : RM {subtotal:.2f}") print(f"SST 6% : RM {tax:.2f}") print(f"Grand : RM {grand:.2f}")
Show the answer
Subtotal : RM 19.50 SST 6% : RM 1.17 Grand : RM 20.67
The :.2f after the variable name forced two decimal places, no matter what the number was. 19.5 became 19.50.
Inside a brace, everything after a colon is a format specifier — instructions for how to format the value. A tiny language of its own.
New Concept · The Mini Format Language
14 minThe shape
{ value : [fill][align][width][,][.precision][type] }
└────┬─────┘ └─┬─┘ └────┬────┘ └─┬─┘
alignment, optional width thousands decimals type
separator (.2f) (f, d, ...)You won't use every part on every value. The four you'll meet today are:
.2f— "two decimal places, fixed-point".10— "minimum 10 characters wide".>10,<10,^10— width plus alignment.03— width 3, padded with leading zeros.,— thousands separator.
Decimal places — .2f
For money, always force two decimals. .2f means "fixed point, two digits after the dot".
pi = 3.14159265 print(f"{pi}") # → 3.14159265 print(f"{pi:.2f}") # → 3.14 print(f"{pi:.4f}") # → 3.1416 print(f"{8:.2f}") # → 8.00 (yes, even on a whole number)
Width — 10
A bare number after the colon is the minimum width. Shorter strings get padded with spaces.
print(f"|{'Aisyah':10}|") # → |Aisyah | (10 chars; left by default for strings) print(f"|{42:5}|") # → | 42| (5 chars; right by default for numbers)
Alignment — < > ^
Use a symbol before the width to force the alignment:
name = "Wei Jie" print(f"|{name:<10}|") # → |Wei Jie | left print(f"|{name:>10}|") # → | Wei Jie| right print(f"|{name:^10}|") # → | Wei Jie | centred
Together with .2f for money, you can finally build aligned receipts:
print(f"|{'Item':<14}|{'Price':>8}|") print(f"|{'Nasi lemak':<14}|{8:>8.2f}|") print(f"|{'Teh tarik':<14}|{3.5:>8.2f}|")
|Item | Price| |Nasi lemak | 8.00| |Teh tarik | 3.50|
Leading zeros — 03
For ID-style numbers that should always have a fixed number of digits, use 0 as the fill character:
for n in range(1, 6): print(f"P{n:03}") # → P001 # P002 # P003 # P004 # P005
Thousands separator — ,
A bare comma turns a long number into 1,000,000.
big = 1234567 print(f"{big}") # → 1234567 print(f"{big:,}") # → 1,234,567 money = 1234.5 print(f"RM {money:,.2f}") # → RM 1,234.50 (combine: comma + decimals)
Money (2 dp) {value:.2f} → 8.00
Money + thousands {value:,.2f} → 1,234.50
Padded ID {n:03} → 007
Right-align in 8 chars {n:>8} → " 42"
Left-align in 10 chars {name:<10} → "Aisyah "
Centred in 14 chars {title:^14} → " Hello! "
Percent (× 100, %) {0.75:.0%} → 75%Worked Example · The Receipt, Reformatted
12 minThe story
Reopen yesterday's receipt.py. The prices were off by a decimal place and the columns drifted. Today we fix it with specifiers.
Save as receipt2.py:
Code
# receipt2.py — receipt with alignment + decimals customer = "Aisyah" items = [ ("Nasi lemak", 8.00, 1), ("Teh tarik", 3.50, 2), ("Roti planta", 4.50, 1), ("Cendol", 5.00, 3), ] SST = 0.06 print(f"{'PAK CIK RAZIF WARUNG':^32}") print(f"{'Customer: ' + customer:^32}") print("-" * 32) print(f"{'Item':<16}{'Qty':>5}{'Total':>11}") print("-" * 32) subtotal = 0 for name, price, qty in items: line_total = price * qty subtotal += line_total print(f"{name:<16}{qty:>5}{line_total:>11.2f}") tax = subtotal * SST grand = subtotal + tax print("-" * 32) print(f"{'Subtotal':<21}{subtotal:>11.2f}") print(f"{'SST 6%':<21}{tax:>11.2f}") print(f"{'GRAND TOTAL':<21}{grand:>11.2f}") print() print(f"{'Thank you, ' + customer + '!':^32}")
Output
PAK CIK RAZIF WARUNG
Customer: Aisyah
--------------------------------
Item Qty Total
--------------------------------
Nasi lemak 1 8.00
Teh tarik 2 7.00
Roti planta 1 4.50
Cendol 3 15.00
--------------------------------
Subtotal 34.50
SST 6% 2.07
GRAND TOTAL 36.57
Thank you, Aisyah!Read the diff
Four specifiers in one file: :^32 centres a banner, :<16 left-aligns the item column, :>5 right-aligns the quantity, and :>11.2f right-aligns the prices with two decimals. The dashes-line "-" * 32 uses Level-1 string multiplication.
The header line and the body line must use the same widths or the columns will drift. 16 + 5 + 11 = 32 — same as the dash count. Drawing tables is a numbers game.
Try It Yourself
13 minYou have prices = [3.5, 12.99, 0.4, 100, 8.555]. Print each price on its own line, formatted as RM 3.50 — right-aligned in a 6-wide field, two decimals.
Hint
prices = [3.5, 12.99, 0.4, 100, 8.555] for p in prices: print(f"RM {p:>6.2f}")
Notice that 8.555 becomes 8.56 — Python rounds when it truncates the extra digits.
Print product IDs P001 through P010 on one line each — using :03 on the loop counter.
Hint
for n in range(1, 11): print(f"P{n:03}") # → P001 # P002 # ... # P010
Given:
scores = [("Priya", 95), ("Wei Jie", 87), ("Aisyah", 92), ("Iman", 70), ("Aizat", 81)]
Print a sorted leaderboard with three columns: rank (3 wide), name (10 wide left), score (5 wide right). Sort by score descending.
Hint
scores = [("Priya", 95), ("Wei Jie", 87), ("Aisyah", 92), ("Iman", 70), ("Aizat", 81)] ranked = sorted(scores, key=lambda s: s[1], reverse=True) print(f"{'#':<3}{'Name':<10}{'Score':>5}") print("-" * 18) for i, (name, score) in enumerate(ranked, start=1): print(f"{i:<3}{name:<10}{score:>5}")
The enumerate(..., start=1) is from PY-L1-13 — it numbers the rows 1-based. (name, score) in the loop is tuple unpacking from PY-L2-04.
Mini-Challenge · The Stock Report
8 minYou've got the warehouse inventory from PY-L2-13 — a list of dicts. Build stock_report.py that prints a polished report.
products = [ {"sku": 1, "name": "pencil", "price": 0.50, "qty": 120}, {"sku": 2, "name": "eraser", "price": 0.80, "qty": 60}, {"sku": 12, "name": "ruler 30cm", "price": 1.20, "qty": 18}, {"sku": 99, "name": "sharpener", "price": 1.50, "qty": 8}, {"sku": 7, "name": "book", "price": 3.00, "qty": 20}, {"sku": 24, "name": "marker pen", "price": 12.50, "qty": 5}, ]
Your file must produce something like this (use :03 for the SKU, two decimals on prices, right-align numeric columns):
============================================ STOCK REPORT (6 lines) ============================================ SKU | Name | Price | Qty | Value ---------------------------------------- P001 | pencil | 0.50 | 120 | 60.00 P002 | eraser | 0.80 | 60 | 48.00 P012 | ruler 30cm | 1.20 | 18 | 21.60 P099 | sharpener | 1.50 | 8 | 12.00 P007 | book | 3.00 | 20 | 60.00 P024 | marker pen | 12.50 | 5 | 62.50 ---------------------------------------- TOTAL | 264.10
Pick your own column widths — but the data rows and the header line must use the same widths.
Stretch goal. Add a thousands separator (,) to the total. Useful when the warehouse gets big.
Show one possible solution
# stock_report.py — formatted stock report products = [ {"sku": 1, "name": "pencil", "price": 0.50, "qty": 120}, {"sku": 2, "name": "eraser", "price": 0.80, "qty": 60}, {"sku": 12, "name": "ruler 30cm", "price": 1.20, "qty": 18}, {"sku": 99, "name": "sharpener", "price": 1.50, "qty": 8}, {"sku": 7, "name": "book", "price": 3.00, "qty": 20}, {"sku": 24, "name": "marker pen", "price": 12.50, "qty": 5}, ] print("=" * 44) print(f" STOCK REPORT ({len(products)} lines)") print("=" * 44) print(f"{'SKU':<5}| {'Name':<12} | {'Price':>5} | {'Qty':>4} | {'Value':>7}") print("-" * 44) total = 0 for p in products: value = p["price"] * p["qty"] total += value print(f"P{p['sku']:03} | " f"{p['name']:<12} | " f"{p['price']:>5.2f} | " f"{p['qty']:>4} | " f"{value:>7.2f}") print("-" * 44) print(f"{'TOTAL':<33}| {total:>7,.2f}")
Non-negotiables: :03 for the SKU, :<12 for the name, two decimals on every monetary value, right-aligned numeric columns, and a final TOTAL line using :,.2f.
Recap
3 minInside an f-string brace, anything after a colon is a format specifier. .2f forces two decimal places. A bare number sets a minimum width — <, > and ^ control alignment. 03 pads numbers with leading zeros. , adds a thousands separator. Combine them — {value:>10,.2f} right-aligns a 10-wide field with thousands and 2 decimals. These specifiers are the single biggest jump from "raw output" to "polished report".
Vocabulary Card
- format specifier
- The bit after a
:inside an f-string brace. - .Nf
- Fixed-point format with
Ndecimal places. - :N
- Minimum width of
Ncharacters. - < / > / ^
- Left / right / centre alignment.
- :0N
- Width
N, padded with leading zeros (use for IDs). - ,
- Thousands separator.
{1234567:,}→1,234,567.
Homework
4 minBuild marks_table.py. You're given the class scores from PY-L2-11. Print a clean 4-column report and a per-subject summary.
class_data = [ {"name": "Aisyah", "maths": 88, "english": 92, "science": 85}, {"name": "Wei Jie", "maths": 74, "english": 68, "science": 91}, {"name": "Priya", "maths": 95, "english": 97, "science": 96}, {"name": "Iman", "maths": 55, "english": 60, "science": 58}, {"name": "Aizat", "maths": 82, "english": 78, "science": 80}, {"name": "Hafiz", "maths": 65, "english": 72, "science": 70}, ]
Your file must:
- Print a header row with columns: Name, Maths, English, Science, Total. Pick widths that fit your data.
- Loop the list and print one row per student. Right-align every number; left-align the name.
- Print a separator line of dashes.
- Print a final row labelled
AVGwith the average of each subject — to 1 decimal place — and the average of the totals.
Stretch. Sort by total descending before printing. Use sorted(class_data, key=lambda s: s["maths"]+s["english"]+s["science"], reverse=True).
Sample · marks_table.py
# marks_table.py — class report with width + decimals class_data = [ {"name": "Aisyah", "maths": 88, "english": 92, "science": 85}, {"name": "Wei Jie", "maths": 74, "english": 68, "science": 91}, {"name": "Priya", "maths": 95, "english": 97, "science": 96}, {"name": "Iman", "maths": 55, "english": 60, "science": 58}, {"name": "Aizat", "maths": 82, "english": 78, "science": 80}, {"name": "Hafiz", "maths": 65, "english": 72, "science": 70}, ] # Add a total field for s in class_data: s["total"] = s["maths"] + s["english"] + s["science"] # Stretch — sort class_data = sorted(class_data, key=lambda s: s["total"], reverse=True) print(f"{'Name':<10}{'Maths':>7}{'English':>9}{'Science':>9}{'Total':>7}") print("-" * 42) for s in class_data: print(f"{s['name']:<10}{s['maths']:>7}{s['english']:>9}{s['science']:>9}{s['total']:>7}") print("-" * 42) n = len(class_data) print(f"{'AVG':<10}" f"{sum(s['maths'] for s in class_data) / n:>7.1f}" f"{sum(s['english'] for s in class_data) / n:>9.1f}" f"{sum(s['science'] for s in class_data) / n:>9.1f}" f"{sum(s['total'] for s in class_data) / n:>7.1f}")
Non-negotiables: per-row aligned f-string, 1 decimal place on the averages, and at least one separator dashed line. The sum(s['maths'] for s in class_data) is a generator expression — same syntax as a list comprehension but with round brackets; we'll learn that name properly in Level 3.