1.2.13 · D4Introduction to Programming (Python)

Exercises — f-strings — embedding expressions

2,467 words11 min readBack to topic

Level 1 — Recognition

Goal: tell what an f-string does at a glance, before writing any.

Exercise 1.1

Which of these prints the number 5, and which prints the literal text {2 + 3}?

A = "{2 + 3}"
B = f"{2 + 3}"
Recall Solution
  • A has no f prefix, so the braces are ordinary characters. print(A) shows {2 + 3}.
  • B has the f, so the braces hold an expression. Python computes 2 + 3 = 5, converts to text, and inserts it. print(B) shows 5.

Rule used: the f is the switch that says "treat { } as code, not characters."

Exercise 1.2

Given name = "Ada", what does each line print?

print("Hello {name}")
print(f"Hello {name}")
Recall Solution
  • Line 1: no f → prints Hello {name} (braces are just letters).
  • Line 2: has f → the braces run the expression name, which is "Ada". Prints Hello Ada.

Exercise 1.3

True or false: "Everything outside the braces in an f-string is copied literally."

Recall Solution

True. The interpreter copies literal characters straight through and only pauses to evaluate the chunks inside { }. This is exactly the left-to-right scan from the parent note.


Level 2 — Application

Goal: write f-strings that embed variables, arithmetic, calls, and indexing.

Exercise 2.1

a, b = 8, 3. Write one f-string that prints exactly: 8 - 3 = 5.

Recall Solution
a, b = 8, 3
print(f"{a} - {b} = {a - b}")   # prints: 8 - 3 = 5

The - between the first two braces is a literal character. The last brace {a - b} is a full expression → 5.

Exercise 2.2

word = "hello". Print HELLO has 5 letters. Use only expressions inside the braces (no extra variables).

Recall Solution
word = "hello"
print(f"{word.upper()} has {len(word)} letters")
# HELLO has 5 letters

word.upper() is a method call (→ "HELLO"), and len(word) is a function call (→ 5). Both are valid expressions, so both live happily in braces.

Exercise 2.3

nums = [10, 20, 30]. Print First: 10, Last: 30 using indexing inside braces.

Recall Solution
nums = [10, 20, 30]
print(f"First: {nums[0]}, Last: {nums[-1]}")
# First: 10, Last: 30

nums[0] is the first element; nums[-1] is the last (negative index counts from the end). Indexing is an expression, so it runs inside the braces.

Exercise 2.4

price = 49.5, qty = 3. Print Total: 148.5.

Recall Solution
price, qty = 49.5, 3
print(f"Total: {price * qty}")   # Total: 148.5

Python multiplies first (49.5 * 3 = 148.5), then converts the number to text. Order matters: evaluate → convert → insert.


Level 3 — Analysis

Goal: predict output when format specs, conversion flags, debug =, and escaping interact.

Exercise 3.1

Predict the output of each line.

pi = 3.14159
print(f"{pi:.3f}")
print(f"{pi:.0f}")
print(f"{255:x}")
print(f"{7:04d}")
Recall Solution
  • {pi:.3f}3.142 — float with 3 decimals. The digit after the 3rd decimal is 5 (from 3.14159), and the digits beyond it (9) push it up, so 3.1415… rounds to 3.142.
  • {pi:.0f}30 decimals, so just the rounded whole number (the fractional part .14… rounds down).
  • {255:x}ffx means lowercase hexadecimal; 255 in base 16 is ff.
  • {7:04d}0007d = decimal integer, 04 = pad to width 4 with zeros.

Everything after the : is the format spec — the "costume" the value wears.

Exercise 3.2

Same value 10, four different type codes. Predict each output.

print(f"{10:b}")
print(f"{10:o}")
print(f"{10:x}")
print(f"{1500:.2e}")
Recall Solution
  • {10:b}1010b = binary (base 2); ten is 1010.
  • {10:o}12o = octal (base 8); ten is 12 (one eight + two ones).
  • {10:x}ax = hexadecimal (base 16); ten is the digit a.
  • {1500:.2e}1.50e+03e = scientific notation with 2 decimals: 1.50 × 10³.

Takeaway: the single trailing letter of a spec picks the base or notation the number is shown in.

Exercise 3.3

The g type and the special conversion flags !r / !s. Predict each output.

print(f"{0.0001234:g}")
print(f"{123456.0:g}")
print(f"{'hi':!r}"[0])   # see solution for why the [0]
name = "Ada"
print(f"{name!r}")
print(f"{name!s}")
Recall Solution
  • {0.0001234:g}0.0001234g = general format; it picks fixed or scientific automatically and trims trailing zeros. Small-but-not-tiny stays fixed.
  • {123456.0:g}123456g drops the useless trailing .0.
  • {name!r}'Ada' — the conversion flag !r calls repr() on the value before formatting. repr() of a string keeps the quotes, so you see 'Ada' (with quotes).
  • {name!s}Ada!s calls str() (the default), so no quotes.
  • {'hi':!r} is not valid — a conversion flag comes before the colon: {'hi'!r}. The middle line above is intentionally a distractor; the correct syntax is shown by the name!r line. (Ignore the [0] — it was there to tempt you into thinking the broken spec runs.)

Rule: !r, !s, !a sit before the : — they choose which text-conversion function runs (repr, str, ascii), while the spec after : chooses the layout.


Level 4 — Synthesis

Goal: combine embedding + specs + conversion flags to hit an exact target output.

Exercise 4.1

amount = 1234567.891. Produce exactly: $1,234,567.89.

Recall Solution
amount = 1234567.891
print(f"${amount:,.2f}")
# output:  $1,234,567.89

The spec ,.2f combines two things: , adds thousands separators, .2f fixes 2 decimals. The $ is literal text outside the braces. Rounding ...91 at 2 decimals gives ...89.

Exercise 4.2

name = "Bo". Print the name right-aligned in a field 6 characters wide, filled with dots, so the output is ....Bo.

Recall Solution
name = "Bo"
print(f"{name:.>6}")   # ....Bo

Reading the spec .>6: the fill character is ., the alignment is > (right-align), and the total width is 6. "Bo" is 2 chars, so 4 dots pad the left.

Exercise 4.3

Given score = 0.8734, print a percentage with one decimal: 87.3\%.

Recall Solution
score = 0.8734
print(f"{score:.1%}")
# output:  87.3%

The % type multiplies by 100 and appends a percent sign; .1 keeps one decimal. 0.8734 → 87.34 → 87.3%. Write a plain % in the spec — no backslash, no escaping.

Exercise 4.4

err = "Not found". You want your log line to show the string with visible quotes so whitespace is obvious: Raised: 'Not found'. Use a conversion flag, not manual quotes.

Recall Solution
err = "Not found"
print(f"Raised: {err!r}")
# output:  Raised: 'Not found'

{err!r} calls repr() on the value before inserting it. For a string, repr() adds surrounding quotes, which is exactly what makes leading/trailing spaces visible in logs. Doing it by hand (f"Raised: '{err}'") works for plain text but breaks the moment the value itself contains a quote — !r handles escaping for you.


Level 5 — Mastery

Goal: reason about evaluation order, edge cases, and equivalence to .format().

Exercise 5.1 (equivalence)

Rewrite f"Hi {name}, score {score:.1f}" using the older str.format() method, and confirm both produce the same text for name="Ada", score=9.25.

Recall Solution
name, score = "Ada", 9.25
a = f"Hi {name}, score {score:.1f}"
b = "Hi {}, score {:.1f}".format(name, score)
assert a == b == "Hi Ada, score 9.2"   # 9.25 rounds to 9.2 (round-half-to-even)

Each {expr:spec} in the f-string maps to a {} placeholder plus a positional argument in .format(). Same underlying format(value, spec) call → identical output. Here 9.25 is an exact half at the 1-decimal place, so Python's round-half-to-even rule kicks in and rounds toward the even digit 2, giving 9.2.

Exercise 5.2 (evaluation timing)

What does this print, and in what order do the two calls happen?

def tick():
    tick.n += 1
    return tick.n
tick.n = 0
print(f"{tick()} then {tick()}")
Recall Solution

Prints 1 then 2. The interpreter scans left to right: it hits the first {tick()}, runs it (returns 1), inserts it, then hits the second {tick()}, runs it (returns 2). Left-to-right evaluation is guaranteed, so the counter increments in reading order.

Exercise 5.3 (nested field width)

width = 8, val = 42. Print 42 padded to a width taken from the variable width, right-aligned: 42 (6 spaces then 42).

Recall Solution
width, val = 8, 42
print(f"{val:>{width}}")   # '      42'

The spec itself contains a nested brace {width}: Python evaluates it first (→ 8), building the spec >8, then applies it. So 42 (2 chars) gets 6 leading spaces to reach width 8.

Exercise 5.4 (degenerate / edge cases)

Predict the output of each, including the empty and zero cases.

print(f"{'':>3}|")
print(f"{0:03d}")
print(f"{-5:+d}")
print(f"{'':}")
Recall Solution
  • f"{'':>3}|" | — an empty string right-aligned in width 3 is just 3 spaces, then the literal |.
  • f"{0:03d}"000 — zero padded to width 3 is 000; the value being zero doesn't change padding rules.
  • f"{-5:+d}"-5 — the + sign flag forces a sign; for a negative number that sign is already -, so you see -5 (for a positive number +d would show +5).
  • f"{'':}" → `` (empty) — an empty spec after the colon means "no formatting," so an empty string stays empty.

Takeaway: every spec still applies at the boundaries — empty strings and zeros obey the same width/sign rules, they just produce short or all-zero output.


  1. What character activates an f-string? ::: The prefix f (or F) immediately before the opening quote.
  2. Spec order in the mini-language? ::: fill·align, sign, #, 0, width, ,, .precision, type — separators before precision, type last.
  3. What do !r, !s, !a do and where? ::: They pick the text-conversion function (repr, str, ascii) and sit before the colon, e.g. {x!r}.
  4. How to use a variable as the width? ::: Nest a brace inside the spec, e.g. f"{val:>{width}}".
  5. Order of evaluation of embedded expressions? ::: Strictly left to right, in source order.

Connections

  • f-strings — embedding expressions — the parent concept these exercises drill.
  • Format specification mini-language — everything after the : in L3–L5, including type codes.
  • str.format() method — the equivalence target in Exercise 5.1.
  • str() and type conversion — why !s vs !r differ (str vs repr).
  • Variables and scope — nested-width and debug forms read the current scope.
  • print() function — the consumer of every example above.