Intuition What this page is
The parent note taught you the rule of f-strings. Here we hunt every case class an f-string can throw at you — every kind of thing you might jam between the braces, every way it can go wrong, every "costume" after the colon — and we work one full example for each cell. By the end you should never meet an f-string scenario you haven't already seen.
See the parent for the core rule: f-strings — embedding expressions .
Before any example, let's map the whole territory. An f-string has exactly three moving parts : the f prefix, the text outside braces, and the expression (plus optional format spec) inside braces. Every scenario is just a different thing living in one of those slots.
#
Case class
What varies
Example we hit it with
A
Plain variable
simplest possible braces
Ex 1
B
Arithmetic / full expression
operators inside braces
Ex 2
C
Calls & indexing
.method(), [i] inside braces
Ex 3
D
Format spec after : — floats
rounding, decimals
Ex 4
E
Format spec — padding & alignment
width, fill, <^>
Ex 5
F
Degenerate / empty inputs
empty string, zero, None
Ex 6
G
Literal braces (escaping)
you WANT a { in output
Ex 7
H
The = debug form
self-documenting output
Ex 8
I
Real-world word problem
receipt / units
Ex 9
J
Exam-style twist
evaluation order + nesting
Ex 10
The chalkboard figure below shows how the interpreter physically walks a string and lands in each of these slots.
Now every cell, one at a time.
Worked example Ex 1 — Cell A: plain variable
planet = "Mars"
print ( f "Next stop: { planet } " )
Forecast: Guess the printed line before reading on.
Python sees the f prefix → braces are now live .
Why this step? Without f, {planet} would print as four literal characters. The f is the switch.
It copies Next stop: literally to the output.
Why this step? Text outside braces is never touched — it is pure literal.
It hits {planet}, evaluates the expression planet in the current scope, gets the string "Mars".
Why this step? The braces hold an expression ; the plainest expression is just a variable name. Scope matters — see Variables and scope .
It converts the result with str() (already a string, so unchanged) and appends it.
Output: Next stop: Mars
Verify: Length check — "Next stop: " is 11 chars, "Mars" is 4 → total 15 chars. Count them; matches.
Worked example Ex 2 — Cell B: arithmetic / full expression
hours = 3
rate = 12.5
print ( f "You earned { hours * rate } rupees" )
Forecast: Will it print 3 * 12.5 or a number? What number?
Literal You earned is copied out.
{hours * rate} is a complete expression . Python does the multiplication first : 3 * 12.5 = 37.5.
Why this step? Braces evaluate the whole expression to a single value before any text conversion. The value is what gets inserted, never the source code.
str(37.5) → "37.5", appended. Then literal rupees.
Output: You earned 37.5 rupees
Verify: 3 × 12.5 = 37.5. An int × float yields a float, hence the .5 decimal, not 37.
Worked example Ex 3 — Cell C: method calls and indexing
name = "ada lovelace"
print ( f " { name.title() } — initials { name[ 0 ] }{ name[ 4 ] } " )
Forecast: What are name[0] and name[4]?
{name.title()} calls the string method .title() → "Ada Lovelace".
Why this step? Any expression is legal, including method calls. See Strings — literals and quotes .
{name[0]} indexes position 0 (Python counts from zero ) → "a".
Why this step? Index 0 is the first character; this is the classic off-by-one trap.
{name[4]} — count a(0) d(1) a(2) space(3) l(4) → "l".
Why this step? The space between words is a real character occupying index 3.
Output: Ada Lovelace — initials al
Verify: "ada lovelace"[0] == "a" and [4] == "l"; .title() capitalises each word's first letter.
Worked example Ex 4 — Cell D: format spec for floats
import math
print ( f "pi to 4 places: { math.pi :.4f } " )
print ( f "third: {1 / 3 :.2f } " )
print ( f "rounds up: {2.678 :.1f } " )
Forecast: Does .1f on 2.678 give 2.6 or 2.7?
After the : comes a format specification — see Format specification mini-language . .4f means "float, 4 digits after the decimal point".
Why this step? The spec is passed to format(value, spec), which does the rounding/padding. The parent's rule: {expr:spec} → format(expr, spec).
math.pi = 3.141592... → rounded to 4 places → 3.1416.
1/3 = 0.3333... → .2f → 0.33.
2.678 → .1f → rounds the second decimal 7 up → 2.7 .
Why this step? .1f rounds to the nearest, not truncates. .67... is closer to .7.
Output:
pi to 4 places: 3.1416
third: 0.33
rounds up: 2.7
Verify: round(math.pi, 4) == 3.1416, round(1/3, 2) == 0.33, round(2.678, 1) == 2.7.
Worked example Ex 5 — Cell E: padding & alignment
print ( f "| { 'hi' :<8 } |" ) # left align, width 8
print ( f "| { 'hi' :>8 } |" ) # right align, width 8
print ( f "| { 'hi' :^8 } |" ) # centre, width 8
print ( f "| {42 : *^ 8} |" ) # centre, fill with *
print ( f " {7 :05d } " ) # zero-pad integer to width 5
Forecast: Where do the spaces land for <, >, ^?
A number after : is a minimum width . <, >, ^ set alignment: left, right, centre.
Why this step? Width alone right-aligns numbers and left-aligns strings by default; the arrow makes it explicit and portable.
'hi':<8 → hi then 6 spaces → hi (8 wide).
'hi':>8 → 6 spaces then hi → hi.
'hi':^8 → 3 spaces hi 3 spaces → hi .
42:*^8 — a fill character goes before the alignment arrow → ***42***.
7:05d → d = integer, 0 fill, width 5 → 00007.
Output:
|hi |
| hi|
| hi |
|***42***|
00007
Verify: All five strings have their stated visible width (8 inside the bars, 5 for the last).
Worked example Ex 6 — Cell F: degenerate & empty inputs
empty = ""
zero = 0
nothing = None
print ( f "[ { empty } ]" ) # empty string
print ( f "count = { zero } " ) # zero, not falsy-hidden
print ( f "result: { nothing } " ) # None
print ( f " { zero :.2f } " ) # zero as float
Forecast: Does an empty string vanish? Does None error?
{empty} inserts str("") → the empty string, so nothing appears between the brackets → [].
Why this step? f-strings don't skip "falsy" values; they always insert str(value), and str("") is genuinely empty text.
{zero} → str(0) → "0". A zero is a perfectly real value, printed as 0.
Why this step? Common bug expectation: people fear 0 disappears. It does not — only empty text is empty.
{nothing} → str(None) → the literal word "None". See str() and type conversion .
Why this step? None has a string form; it never raises inside an f-string.
{zero:.2f} → format(0, ".2f") → "0.00".
Output:
[]
count = 0
result: None
0.00
Verify: str("") == "", str(0) == "0", str(None) == "None", format(0, ".2f") == "0.00".
Worked example Ex 7 — Cell G: you WANT a literal brace
lang = "Python"
print ( f " {{{ lang }}} " ) # a set-like display
print ( f "Use {{ }} for blanks" )
Forecast: How many braces of each do you need to print one {?
A single { starts an expression, so to print one literal { you double it: {{.
Why this step? The parser reads {{ as an escape for one output {, the same trick as %% for percent signs elsewhere.
f"{{{lang}}}" splits as {{ (→ {) + {lang} (→ Python) + }} (→ }).
Why this step? Count them in pairs from the outside: two-then-one-then-two. The lone middle pair is the real expression.
Second line: {{ }} → { } literally.
Output:
{Python}
Use { } for blanks
Verify: f"{{{lang}}}" == "{Python}" when lang == "Python".
Worked example Ex 8 — Cell H: the
= debug form (Python 3.8+)
width = 4
height = 6
print ( f " { width = } " )
print ( f " { width * height = } " )
print ( f " { width * height = :.1f } " )
Forecast: Does {width=} print 4 or width=4?
{expr=} prints both the source text of the expression and its value.
Why this step? It's built for debugging — you instantly see what produced which value.
{width=} → the text width, then =, then 4 → width=4.
{width * height = } — spaces around = are preserved in the output → width * height = 24.
You can still add a spec: {width * height = :.1f} → width * height = 24.0.
Why this step? The = and the :spec combine — value part still gets formatted.
Output:
width=4
width * height = 24
width * height = 24.0
Verify: 4 * 6 == 24 and format(24, ".1f") == "24.0".
Worked example Ex 9 — Cell I: real-world word problem (a receipt)
item = "USB cable"
qty = 3
unit_price = 149.5
subtotal = qty * unit_price
tax = subtotal * 0.18
total = subtotal + tax
print ( f " { item :<12 }{ qty :>3 } x { unit_price :>8,.2f } " )
print ( f " { 'Subtotal' :<15 }{ subtotal :>8,.2f } " )
print ( f " { 'Tax (18%)' :<15 }{ tax :>8,.2f } " )
print ( f " { 'TOTAL' :<15 }{ total :>8,.2f } " )
Forecast: What's the total, to 2 decimals?
subtotal = 3 * 149.5 = 448.5.
Why this step? Compute the raw number first, then format it — never format then compute.
tax = 448.5 * 0.18 = 80.73.
total = 448.5 + 80.73 = 529.23.
,.2f combines a thousands separator (,) with 2 decimals (.2f); < / > align columns so the receipt lines up.
Why this step? , and .2f stack in one spec — mini-language grammar allows both. The alignment makes a tidy table.
Note we wrote 18% inside a plain literal (not a spec), so no escaping needed here — the % is just text.
Output:
USB cable 3 x 149.50
Subtotal 448.50
Tax (18%) 80.73
TOTAL 529.23
Verify: subtotal == 448.5, tax == 80.73, total == 529.23, and format(total, ",.2f") == "529.23".
Worked example Ex 10 — Cell J: exam twist (evaluation order + nested field)
nums = [ 4 , 1 , 7 , 2 ]
places = 2
print ( f " {sorted (nums)[ - 1 ] : . { places } f } " )
Forecast: Two puzzles: what is sorted(nums)[-1], and what does {places} do inside the spec?
sorted(nums) → [1, 2, 4, 7] (ascending, does not touch nums).
Why this step? sorted returns a new list; the original order is preserved elsewhere.
[-1] indexes the last element → 7.
Why this step? Negative indices count from the end; -1 is always the last item.
The spec is .{places}f — a nested replacement field . Python evaluates {places} → 2 first, producing the real spec .2f.
Why this step? You can compute the format spec dynamically; the inner braces are resolved before formatting.
format(7, ".2f") → "7.00".
Output: 7.00
Verify: sorted([4,1,7,2])[-1] == 7 and format(7, ".2f") == "7.00".
Common mistake The trap in Ex 10's twist
Students expect {sorted(nums)[-1]:.{places}f} to fail because of the "nested braces". It does not — the inner {places} sits inside the spec (after the colon) and is resolved as its own tiny field. This is different from trying to print a literal brace (Ex 7), where you must double them.
Mnemonic One line to carry all cells
"Compute inside, costume after the colon, double to escape, = to debug."
Recall Rapid fire across the matrix
Does f"[{''}]" print [] or [ ]?
Does .1f on 2.678 give 2.6 or 2.7?
What does {42:*^8} produce?
What does sorted([4,1,7,2])[-1] return?
What does {x=} add compared to {x}?
Recall Answers
Empty brackets [] ::: str("") is genuinely empty text.
2.7 ::: .1f rounds to nearest, .67… → .7.
***42*** ::: fill *, centre ^, width 8.
7 ::: -1 is the last item of the sorted list [1,2,4,7].
The expression's source text and an = ::: e.g. x=9 for debugging.
f-strings — embedding expressions — the parent rule these examples exercise.
Format specification mini-language — everything after the : in Ex 4, 5, 9, 10.
str() and type conversion — why None, 0, "" all have safe string forms (Ex 6).
str.format() method — the older {} sibling with the same spec grammar.
Variables and scope — braces evaluate in the current scope (Ex 1).
print() function — the consumer of every line above.
Cell G double brace escape