F-Strings: Modern String Formatting
1 · The lesson
readF-strings (formatted string literals) were added in Python 3.6. They are the standard way to build strings in modern Python. They are faster than the older styles, they read from left to right, and each expression sits in the exact place where its value appears in the output.
1. Before F-Strings
Before f-strings, Python had two formatting systems. You'll still see both in older codebases.
name = "Margaret" age = 30 # 1. %-formatting (Python 2 era, modelled on C's printf) print("Hi, %s. You are %d." % (name, age)) # 2. str.format() (Python 2.6+, positional or named placeholders) print("Hi, {}. You are {}.".format(name, age)) print("Hi, {n}. You are {a}.".format(n=name, a=age)) # 3. f-strings (Python 3.6+), the modern way print(f"Hi, {name}. You are {age}.")
All three produce the same output. The f-string is the only one where the value is written inside the placeholder. With the other two, you have to match each placeholder to an argument at the end of the line.
2. Any Expression Goes Inside {}
The braces can hold more than a variable name. Python evaluates any expression you put inside them.
x, y = 3, 4 print(f"{x + y}") # '7' (arithmetic) print(f"{x * y + 1}") # '13' name = "surya" print(f"{name.upper()}") # 'SURYA' (a method call) items = ["apples", "bread", "milk"] print(f"First: {items[0]}") # 'First: apples' (indexing) print(f"Count: {len(items)}") # 'Count: 3' (a function call) import math print(f"π ≈ {math.pi:.4f}") # 'π ≈ 3.1416' (an attribute plus a format spec)
Keep it readable. If the expression is more than a quick lookup or one operation, compute it on a separate line and interpolate the result.
3. Format Specifiers
After a colon inside the braces comes the format spec. The format spec controls how the value is displayed: width, alignment, decimal places and more.
Width and Alignment
name = "Margaret" print(f"|{name:>10}|") # '| Margaret|' (right-aligned, width 10) print(f"|{name:<10}|") # '|Margaret |' (left-aligned) print(f"|{name:^10}|") # '| Margaret |' (centered) print(f"|{name:*^10}|") # '|*Margaret*|' (filled with '*')
You'll use this for tables, receipts, anything that lines up in columns.
Numbers: Decimals, Thousands, Percent, Scientific
pi = 3.14159265 count = 1_234_567 ratio = 0.0847 big = 12_345_000_000 print(f"{pi:.2f}") # '3.14' (2 decimal places) print(f"{pi:.5f}") # '3.14159' (5 decimal places) print(f"{count:,}") # '1,234,567' (thousands separators) print(f"{count:,.2f}") # '1,234,567.00' (both together) print(f"{ratio:.1%}") # '8.5%' (percent, which multiplies by 100) print(f"{big:.2e}") # '1.23e+10' (scientific notation)
Padding Integers with Zeros
Use this for dates, IDs and any other number that should look like 007, not 7.
day = 5 month = 9 year = 2026 print(f"{year}-{month:02d}-{day:02d}") # '2026-09-05' invoice_id = 42 print(f"INV-{invoice_id:06d}") # 'INV-000042'
The 02d means "integer, width 2, padded with zeros on the left."
4. The Debug = Syntax (Python 3.8+)
This syntax is very useful for debugging with print(). Put = after the expression, and the f-string shows both the expression text and its value.
x = 42 y = [1, 2, 3] print(f"{x=}") # 'x=42' print(f"{y=}") # 'y=[1, 2, 3]' print(f"{len(y)=}") # 'len(y)=3' print(f"{x * 2=}") # 'x * 2=84'
When you are debugging and want to know the current value of a variable, f"{variable=}" is the shortest way to print it. You don't have to type the name twice.
# Before: print("user_id:", user_id, "balance:", balance) # After: print(f"{user_id=}, {balance=}")
setup added so this can run · defines user_id, balance
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) user_id = _AutoMock('user_id') balance = _AutoMock('balance')
5. Multi-Line F-Strings
Triple quotes work with the f prefix too. This is useful for templates, prompts and emails.
name = "Margaret" items = 3 total = 47.25 receipt = f""" Customer: {name} Items: {items} Total: ${total:.2f} Thanks for shopping with us! """ print(receipt)
Python also joins string literals that are written next to each other, when it reads the code. This lets you split a long f-string over several lines without using +:
msg = (
f"Order confirmation for {name}: "
f"{items} items totalling ${total:.2f}."
)setup added so this can run · re-runs 4 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) name = _AutoMock('name') items = _AutoMock('items') total = _AutoMock('total') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import math except Exception: pass _pm_earlier = [ 'name = "Margaret"\nage = 30\n\n# 1. %-formatting (Python 2 era, modelled on C\'s printf)\nprint("Hi, %s. You are %d." % (name, age))\n\n# 2. str.format() (Python 2.6+, positional or named placeholders)\nprint("Hi, {}. You are {}.".format(name, age))\nprint("Hi, {n}. You are {a}.".format(n=name, a=age))\n\n# 3. f-strings (Python 3.6+), the modern way\nprint(f"Hi, {name}. You are {age}.")', 'x, y = 3, 4\nprint(f"{x + y}") # \'7\' (arithmetic)\nprint(f"{x * y + 1}") # \'13\'\n\nname = "surya"\nprint(f"{name.upper()}") # \'SURYA\' (a method call)\n\nitems = ["apples", "bread", "milk"]\nprint(f"First: {items[0]}") # \'First: apples\' (indexing)\nprint(f"Count: {len(items)}") # \'Count: 3\' (a function call)\n\nimport math\nprint(f"π ≈ {math.pi:.4f}") # \'π ≈ 3.1416\' (an attribute plus a format spec)', 'name = "Margaret"\n\nprint(f"|{name:>10}|") # \'| Margaret|\' (right-aligned, width 10)\nprint(f"|{name:<10}|") # \'|Margaret |\' (left-aligned)\nprint(f"|{name:^10}|") # \'| Margaret |\' (centered)\nprint(f"|{name:*^10}|") # \'|*Margaret*|\' (filled with \'*\')', 'name = "Margaret"\nitems = 3\ntotal = 47.25\n\nreceipt = f"""\nCustomer: {name}\nItems: {items}\nTotal: ${total:.2f}\nThanks for shopping with us!\n"""\nprint(receipt)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
6. When NOT to Use F-Strings
F-strings interpolate at the moment the f prefix is parsed. If your template is stored in a config file, a database or a variable, the f prefix can't help you. Use str.format() with a dictionary instead:
template = "Hi, {name}. You owe ${amount}." # a plain string, with no 'f' data = {"name": "Margaret", "amount": 42} print(template.format(**data)) # 'Hi, Margaret. You owe $42.'
This is the right tool when:
- the template is data (loaded from a file, edited by a non-programmer),
- the same template is reused with different variables,
- you're building something like an email or notification system.
The Mad Libs project walks through exactly this pattern.
7. Three Gotchas
1. An f-string is filled in only where it is written. Almost everyone makes this mistake once.
template = "Hello, {name}" # a plain string, because the f was forgotten name = "Margaret" print(template) # 'Hello, {name}' (the braces print as they are)
If you want to "store an f-string for later", what you need is str.format(), or a function that builds the string when you call it.
2. No backslashes inside the {...} part.
items = ["a", "b", "c"] # This is a SyntaxError in Python < 3.12: print(f"{'\n'.join(items)}") # Pull the separator out first: sep = "\n" print(f"{sep.join(items)}")
Python 3.12 relaxed this rule (see PEP 701), but if your code needs to run on older versions, keep backslashes outside the braces.
3. Quote-nesting: alternate the styles. Before Python 3.12, the outer quotes couldn't appear inside the expression.
user = {"name": "Margaret"}
# OK: double quotes outside, single quotes inside:
print(f"Name: {user['name']}")
# SyntaxError before Python 3.12, because both quotes are double (3.12+ allows it):
# print(f"Name: {user["name"]}")Pick the opposite quote style for the dict key, attribute access, or string literal inside {...}.
Common Mistakes
- Forgetting the
fprefix."Hi, {name}"is a literal string. Look for the prefix every time you read a template. - Format spec on the wrong side of the colon.
f"{x.2f}"is an attribute access (and an error).f"{x:.2f}"is the format spec. - Using f-strings for user-supplied templates. The only way to turn a user's text into an f-string is
eval(), and then the user can run any code they like. Usestr.format()with a fixed set of allowed keys instead. f"{x = }"vsf"{x=}". Whitespace inside the braces is preserved literally.f"{x = }"produces'x = 42', andf"{x=}"produces'x=42'. Pick one and be consistent.
🎯 Your Turn: Build a Receipt Formatter
Write a function format_invoice(items) that takes a list of (name, qty, price) tuples and returns a right-aligned receipt as a single string.
Target output:
-------------------------------- Apple 3 @ $ 0.50 Coffee 1 @ $ 4.25 Notebook 2 @ $ 3.00 -------------------------------- TOTAL $11.75
Rules:
1. Item name is left-aligned in a 20-character column.
2. Quantity is right-aligned in a 3-character column.
3. Price is shown with 2 decimal places after $.
4. The total goes on the last line, right-aligned with the prices, with 2 decimals.
5. Top and bottom rules are 32 dashes.
Skeleton:
def format_invoice(items): lines = [] lines.append("-" * 32) total = 0 for name, qty, price in items: # TODO: build the line for this item # TODO: update the running total pass lines.append("-" * 32) # TODO: append the total line return "\n".join(lines) cart = [ ("Apple", 3, 0.50), ("Coffee", 1, 4.25), ("Notebook", 2, 3.00), ] print(format_invoice(cart))
Hint 1: One line per item
For each tuple, the line is just one f-string with three format specs:line = f"{name:<20}{qty:>3} @ ${price:>5.2f}"
The subtotal for the line is qty * price. Add it to the running total before you append the line.
Hint 2: Aligning the total
The total line needs the word"TOTAL" on the left and the dollar amount right-aligned. Width 32 with the total at the end:
lines.append(f"{'TOTAL':<20}{'':>6}${total:>5.2f}")
Or just compute the right-side width as 32 - len("TOTAL") - 1 and use that explicitly.
Show full solution
def format_invoice(items): lines = ["-" * 32] total = 0.0 for name, qty, price in items: subtotal = qty * price total += subtotal lines.append(f"{name:<20}{qty:>3} @ ${price:>5.2f}") lines.append("-" * 32) lines.append(f"{'TOTAL':<20}{'':>6}${total:>5.2f}") return "\n".join(lines) cart = [ ("Apple", 3, 0.50), ("Coffee", 1, 4.25), ("Notebook", 2, 3.00), ] print(format_invoice(cart))
Output:
-------------------------------- Apple 3 @ $ 0.50 Coffee 1 @ $ 4.25 Notebook 2 @ $ 3.00 -------------------------------- TOTAL $11.75
Three format specs do all the alignment work: :<20 for the left-aligned name, :>3 for the right-aligned qty, :>5.2f for the right-aligned dollar amount with two decimals. You don't add any spaces by hand. The format specs do all the padding.
In a real billing system you'd add a currency symbol parameter, handle locales properly (commas vs dots), and probably switch to decimal.Decimal for money. But the formatting structure stays exactly the same.
What You Learned
- F-strings are
f"...{expression:format_spec}...". Thefprefix is mandatory. - Any expression works inside
{}: arithmetic, method calls, indexing. - Format specs after
:handle width (>10,<10,^10), decimals (.2f), thousands (,), percent (.1%), scientific (.2e), zero-padding (02d). f"{x=}"printsx=value. It is the shortest way to print a variable while debugging.- Triple-quoted f-strings work for multi-line templates.
- For templates that are data (config, DB, user-editable), use
str.format(**dict). See Mad Libs.
Next: Lists, the ordered, mutable collection that you will use in most Python programs.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code did, as well as whether a test passed.
Found this useful? Share it with a friend who’s learning Python.