PYTHON / STRINGS
String formatting with f-strings
Use f-strings to embed live expressions in string literals and control precision, width, alignment, and grouping with format specs.
What you will learn
- Embed any Python expression in braces and get an ordinary str back immediately
- Control numbers with :.2f, :, grouping, :05d, and :.1% inside the format spec
- Align text using :<, :>, :^ with a fixed or computed field width
- Use {value!r} for repr and {value=} for one-line debug printing
Understanding String formatting with f-strings
An f-string is a string literal with an f before the opening quote. Everything inside braces is parsed as real Python code when the module is compiled, and evaluated at the moment the literal is reached during execution. The result is an ordinary str with no memory of where its pieces came from, which is why reassigning a variable afterwards never changes an f-string you already built. This is the key difference from a template held in a variable: there is no template left over, only text.
Each replacement field has three parts: {expression!conversion:format_spec}. The expression can be anything that fits in one expression, including calls, indexing, arithmetic, and comprehensions. The optional conversion is !r (repr), !s (str), or !a (ascii); !r is what shows quotes around strings and is invaluable when you need to see whether a value is 3 or "3". The format spec after the colon is the same mini-language used by format(), so :>10 pads to width ten on the right, :.3f fixes three decimals, and :, inserts thousands separators.
Two details make f-strings feel less rigid than they look. First, the format spec may itself contain braces, so f"{name:>{width}}" computes the width at runtime instead of hardcoding it. Second, because the braces are syntax, a literal brace must be doubled: f"{{x}}" prints {x}. Keep in mind that an f-string is eager, so it is the wrong tool when you want a reusable template, a lazily formatted log message, or a string that a user supplies.
name = "Ada"
items = 3
price = 19.5
print(f"{name} bought {items} items")
print(f"Total: {items * price:.2f}")
print(f"{name!r} has {len(name)} letters")
print(f"{items=}, {price=}")
print(f"|{name:>8}|{name:<8}|{name:^8}|")An f-string is a literal whose braces hold real expressions evaluated on the spot, with an optional !conversion and :format_spec deciding how each value is rendered.
Worked examples
Number formatting specs
Shows grouping, fixed decimals, percent conversion, and zero padding on numeric values.
total = 1234567.891
ratio = 0.256
count = 42
print(f"{total:,.2f}")
print(f"{ratio:.1%}")
print(f"{count:05d}")
print(f"{total:15,.0f}|")Example explained
Line 1:,.2f applies grouping and two decimals together; the comma comes before the precision.
Line 2:.1% multiplies by 100, rounds to one decimal, and appends the percent sign for you.
Line 3:05d pads an integer with leading zeros to width 5, which plain str() cannot do.
Line 4A bare width like 15 right-aligns numbers by default, so the trailing | shows where the field ends.
Expressions, containers, and literal braces
Demonstrates indexing, method calls, escaped braces, and a width computed from a variable.
user = {"name": "Bo", "roles": ["admin", "dev"]}
tags = ["x", "y"]
width = 6
print(f"{user['name']} -> {', '.join(user['roles'])}")
print(f"{len(tags)} tag(s): {tags}")
print(f"{{literal braces}} and {tags[0].upper()}")
print(f"[{user['name']:^{width}}]")Example explained
Line 1Inner single quotes let the dictionary key and join argument sit inside a double-quoted f-string.
Line 2{tags} calls str() on the list, and str() of a list shows the repr of each element, hence the quotes.
Line 3{{ and }} produce one literal brace each, so no replacement is attempted there.
Line 4{width} inside the spec is substituted first, turning :^{width} into :^6 before centering.
Aligned table rows
Builds a fixed-width report by giving every column its own alignment and width.
rows = [("apples", 3, 1.25), ("bread", 1, 2.5), ("olive oil", 2, 7.0)]
print(f"{'item':<10}{'qty':>4}{'cost':>9}")
for name, qty, price in rows:
print(f"{name:<10}{qty:>4}{qty * price:>9.2f}")Example explained
Line 1String literals such as 'item' are valid expressions, so headers can use the same specs as the data.
Line 2:<10 left-aligns names into a ten-character column so the next field always starts at the same offset.
Line 3:>9.2f combines alignment and precision, keeping decimal points vertically lined up.
Line 4The multiplication qty * price happens inside the field, so no temporary variable is needed.
Important notes
The debug form f"{x=}" requires Python 3.8+, while same-quote nesting and backslashes inside the expression require 3.12+.
An f-string is evaluated once and cannot be reused as a template, so pass %s-style arguments to logging calls and use parameterized queries instead of f-strings for SQL.
Common mistakes
Omitting the f prefix: "Total: {n}" raises no error and silently prints the braces and the name n instead of the value.
Reusing the outer quote character inside the expression, as in f"{d["key"]}" — this is a SyntaxError on Python 3.11 and earlier, so use f"{d['key']}" for portable code.
Applying an integer-only code to a float, such as f"{2.5:d}" or f"{2.5:,d}", which raises ValueError: Unknown format code 'd' for object of type 'float'; use .0f instead.
Try it yourself
Change, predict, then run
Given product = "widget", qty = 7, unit = 4.5, print one line in the form 'widget x 7 = 31.50' using a left-aligned 10-wide name, a right-aligned 2-wide quantity, and a right-aligned 8-wide total with two decimals, then print a second debug line using {qty=} and {unit=}.
Open the Python workspaceCheck your understanding
What does this print? n = 1 msg = f"{n} items" n = 99 print(msg)
- 1 items
- 99 items
- {n} items
- It raises a NameError because n was rebound
Show answer
The expression n is evaluated when the f-string literal executes, and the result is a plain immutable str, so msg is fixed as "1 items". Answering "99 items" assumes the f-string keeps a live link to the variable and re-renders on every use, which is exactly what f-strings do not do — for deferred substitution you need a stored template plus str.format.