Learning Goals
3 minBy the end of this lesson you can:
- Recognise the
fprefix and drop variables into a string with{name}. - Embed full Python expressions inside braces —
{a + b},{len(items)}, even method calls. - Print a literal brace by typing it twice —
{{and}}. - Choose between f-strings, commas in
print()and string concatenation — and explain why f-strings usually win.
Warm-Up
5 minWe've been writing lines like this all term:
name = "Aisyah" age = 12 print("Hi", name, "— you are", age, "years old.")
Predict what the f-string version below prints:
print(f"Hi {name} — you are {age} years old.")
Show the answer
Hi Aisyah — you are 12 years old.
One line, no commas, no awkward gaps. Whatever's in the curly braces is just Python, evaluated and dropped in.
An f-string is a normal string with small windows in it. Inside a window, any Python expression is allowed. The expression is evaluated and its value is poured in.
New Concept · The f-Prefix and the Braces
14 minThe shape
Add a lower-case f (or F) right before the opening quote. Anything in { } becomes Python.
name = "Wei Jie" score = 95 msg = f"Player {name} scored {score} points!" print(msg) # → Player Wei Jie scored 95 points!
Without the f, the braces would print literally. The prefix is the magic switch.
Any expression goes
The braces don't just hold variable names. They hold any Python expression — maths, function calls, method calls, indexing.
a = 6 b = 7 print(f"{a} times {b} is {a * b}") # → 6 times 7 is 42 print(f"In upper case: {name.upper()}") # → In upper case: WEI JIE scores = [82, 91, 76] print(f"Class average: {sum(scores) / len(scores)}") # → Class average: 83.0
That last one would otherwise need a separate avg = sum(...) / len(...) line. f-strings make tiny calculations feel weightless.
Multi-line f-strings
Triple-quote, then add f. Every brace inside the block becomes a window.
name = "Iman" hp = 50 print(f""" ═════════════════════ Player: {name} HP : {hp} Level : {hp // 10} ═════════════════════ """)
Great for game banners and report cards — much cleaner than four separate print lines.
Printing a literal brace
If you actually want a { or } in the output, type it twice. Python prints one.
x = 42 print(f"The answer is {x} (sometimes written as {{42}}).") # → The answer is 42 (sometimes written as {42}).
f-string vs. comma vs. concatenation
You now know three ways to build a printed line. Read them side by side:
Style Code Output
comma print("Hi", name, "age", age) Hi Aisyah age 12
concatenation print("Hi " + name + " age " Hi Aisyah age 12
+ str(age)) (and: TypeError if you forget str())
f-string print(f"Hi {name} age {age}") Hi Aisyah age 12The f-string wins on three counts: no extra spaces sneaking in (comma puts one between every item), no str() conversions (concatenation can't mix string and number directly), and the layout reads exactly like the output line.
An f-string still has to be a valid string. If you need quotes inside a brace expression, use a different quote-style than the outer string:
fruit = "apple" print(f"I like {fruit.upper()}!") # ✅ fine print(f"First letter: {fruit[0]}") # ✅ fine print(f"Says: {'hello'}") # ✅ inner quotes are different # print(f"Says: {"hello"}") # ❌ SyntaxError — same quotes
Worked Example · The Receipt Printer
12 minThe story
Pak Cik Razif's warung needs a printed receipt for each customer. The old code used commas and looked scruffy. We'll rebuild it with f-strings and a calculation in the braces.
Save as receipt.py:
Code
# receipt.py — order summary with f-strings customer = "Aisyah" items = [ ("Nasi lemak", 8.00, 1), ("Teh tarik", 3.50, 2), ("Roti planta", 4.50, 1), ] SST = 0.06 print(f"=== Pak Cik Razif's Warung ===") print(f"Customer: {customer}") print() subtotal = 0 for name, price, qty in items: line_total = price * qty subtotal += line_total print(f"{name} x{qty} RM {line_total}") tax = subtotal * SST grand = subtotal + tax print() print(f"Subtotal : RM {subtotal}") print(f"SST 6% : RM {tax}") print(f"Grand : RM {grand}") print() print(f"Thank you, {customer}! See you again.")
Output
=== Pak Cik Razif's Warung === Customer: Aisyah Nasi lemak x1 RM 8.0 Teh tarik x2 RM 7.0 Roti planta x1 RM 4.5 Subtotal : RM 19.5 SST 6% : RM 1.17 Grand : RM 20.67 Thank you, Aisyah! See you again.
Read the diff
Two patterns to spot. (1) Tuple unpacking inside the loop — for name, price, qty in items — pulls every piece of each item out by name (PY-L2-04). (2) The braces hold computed values — {price * qty} — saving a separate variable line.
Notice the prices print as RM 8.0 and RM 7.0 instead of RM 8.00 and RM 7.00. f-strings have an answer to that — but it's tomorrow's topic (PY-L2-16, the format specifiers).
Try It Yourself
13 minRead a name and age with input() (remember int() on the age!). Print a one-line f-string greeting that includes both.
Hint
name = input("Name: ") age = int(input("Age : ")) print(f"Hi {name}! You'll be {age + 1} on your next birthday.")
That little {age + 1} is the kind of micro-calculation f-strings shine at — no temp variable needed.
Given scores = [82, 91, 76, 65, 88], print a single f-string line:
5 scores | avg 80.4 | best 91 | worst 65
Hint
scores = [82, 91, 76, 65, 88] print(f"{len(scores)} scores | avg {sum(scores)/len(scores)} | best {max(scores)} | worst {min(scores)}")
Five built-in calls inside one f-string. This is the whole point — the f-string reads almost like the printed line.
Build a multi-line banner for a game character. Given name = "Hero", hp = 75, max_hp = 100, print:
╔═══════════════════════╗ ║ HERO ║ ║ HP: 75 / 100 (75%) ║ ╚═══════════════════════╝
Hint
name = "Hero" hp = 75 max_hp = 100 print(f"""╔═══════════════════════╗ ║ {name.upper()} ║ ║ HP: {hp} / {max_hp} ({hp * 100 // max_hp}%) ║ ╚═══════════════════════╝""")
Two f-string tricks at once: a method call (.upper()) and a maths expression (hp * 100 // max_hp — integer division for a whole-number percent).
Mini-Challenge · Re-write the Number Guesser Recap
8 minBack in PY-L1-48 you printed the final summary of your number-guessing game with a stack of print(...) + comma lines. Today we re-do that summary with f-strings — clean and readable.
Build summary.py. Hard-code these variables at the top (no need for the game loop):
player = "Aisyah" target = 42 guesses = [50, 30, 40, 45, 42] won = True
Your file must use f-strings to print:
- A header line:
=== Game over for Aisyah === - The secret number was 42.
- The number of guesses they took (use
len()). - The list of guesses, comma-separated using
", ".join([str(g) for g in guesses]). - Whether they won — use a one-line conditional inside the brace:
{"YES 🎉" if won else "no 😢"} - An average guess (
sum / len) and the distance from the target for their best guess (minof the absolute differences).
Show one possible solution
# summary.py — number-guesser recap with f-strings player = "Aisyah" target = 42 guesses = [50, 30, 40, 45, 42] won = True print(f"=== Game over for {player} ===") print(f"Secret number : {target}") print(f"Number of tries : {len(guesses)}") print(f"Their guesses : {', '.join([str(g) for g in guesses])}") print(f"Did they win? : {'YES 🎉' if won else 'no 😢'}") print(f"Average guess : {sum(guesses) / len(guesses)}") # Best guess = the one closest to the target distances = [abs(g - target) for g in guesses] print(f"Closest guess off by : {min(distances)}")
Non-negotiables: every visible line uses an f-string, at least one inline calculation, the inline conditional {... if won else ...}, and ", ".join([str(g) for g in guesses]) for the list. Notice the inner string uses single quotes — you have to alternate when the outer string uses doubles.
Recap
3 minAn f-string is a normal string with a leading f and tiny Python windows in curly braces. Whatever you write in the braces is evaluated and dropped in — variables, maths, function calls, method calls, even conditional expressions. f-strings beat commas and concatenation because they read like the line they print, never add stray spaces, and don't need str() conversions. Double the brace ({{) to print a literal one; don't reuse the outer quote-style inside a brace.
Vocabulary Card
- f-string
- A formatted string literal — a string prefixed with
fwhere{...}evaluates Python. - brace / window
- The
{ }inside an f-string where the Python expression lives. - literal brace
- Type
{{for one{, and}}for one}. - format specifier
- The bit after a
:inside a brace, like{price:.2f}— tomorrow's lesson.
Homework
4 minSave profile_card.py. Build a profile card for yourself using a single triple-quoted f-string. Hard-code your own values at the top.
Your file must:
- Define at least four variables (name, year_of_birth, school, three favourite subjects).
- Print one triple-quoted f-string that contains all of them in a bordered card — at least six lines tall.
- Include at least one calculation inside the braces (
{2026 - year_of_birth}for current age). - Include at least one method call inside the braces (
{name.upper()}). - Include at least one list join inside the braces — for the favourite subjects.
Stretch. Add a one-line conditional brace: {"Senior" if age >= 16 else "Junior"}.
Sample · profile_card.py
# profile_card.py — a multi-line bordered profile card name = "Aisyah binti Hassan" born = 2014 school = "SK Taman Connaught 2" subjects = ["maths", "art", "BM"] age = 2026 - born print(f""" ╔════════════════════════════════════════╗ ║ PROFILE — {name.upper()} ║ School : {school} ║ Age : {age} ({"Senior" if age >= 16 else "Junior"}) ║ Loves : {", ".join(subjects)} ║ Born : {born} ╚════════════════════════════════════════╝ """)
Non-negotiables: one triple-quoted f-string, at least one calculation, one method call, one .join(), and at least six lines of body. Your own values are yours.