f-strings are how you build text in modern Python. They arrived in 3.6 and there is almost no reason to use anything else. This post covers the syntax, the format spec that lines up currency and columns, and the two older styles you will still meet.
The basic form
Put f before the quote and write expressions in braces:
name = "Alice"
balance = 9650.5
print(f"Hello, {name}") # Output: Hello, Alice
print(f"You have {balance} in total") # Output: You have 9650.5 in total
print(f"Next year: {balance * 1.05}") # Output: Next year: 10133.025
Anything that is an expression works inside the braces — arithmetic, a method call, an index. What
does not work is a statement, so no assignments and no for loops.
The reason to prefer f-strings over concatenation is not brevity, it is that the value stays where
you read it. f"Hello, {name}" shows you the finished sentence;
"Hello, " + name makes you assemble it in your head — and fails outright if
name is a number.
Formatting numbers
After a colon comes the format spec, and this is where f-strings stop being a convenience and start being the reason you use them.
balance = 9650.5
ratio = 0.0725
print(f"{balance:.2f}") # Output: 9650.50
print(f"{balance:,.2f}") # Output: 9,650.50
print(f"${balance:,.2f}") # Output: $9,650.50
print(f"{ratio:.1%}") # Output: 7.2%
print(f"{255:x}") # Output: ff
.2f fixes it at two decimal places — note it gave you 9650.50, with the
trailing zero the raw float does not have. , inserts thousands separators.
% multiplies by 100 and appends the sign. Those three cover almost every number you will
ever display to a person.
Aligning columns
The same spec does padding, which is how you produce a readable table without a library:
rows = [("Deposit", 100.5), ("Withdrawal", -25.0), ("Transfer in", 250.0)]
for label, amount in rows:
print(f"{label:<14}{amount:>10,.2f}")
# Output: Deposit 100.50
# Output: Withdrawal -25.00
# Output: Transfer in 250.00
< left-aligns, > right-aligns and ^ centres, each
followed by a width. Numbers right-aligned to a fixed width put every decimal point in the same
column, which is the whole trick to a statement that looks professional.
Put any character before the alignment to pad with it instead of spaces —
f"{'':-<20}" draws a twenty-character rule.
The = that debugs for you
Add = after the expression and the f-string prints the expression itself along with its
value:
quantity = 3
price = 15.5
print(f"{quantity=}") # Output: quantity=3
print(f"{quantity * price=}") # Output: quantity * price=46.5
This exists precisely so you stop writing print("quantity:", quantity) and getting the
label out of step with the variable. It arrived in 3.8 and it is the fastest debugging tool in the
language. Debugging covers what to use when this is not
enough.
Calling things inside the braces
user = {"full_name": "alice cooper", "accounts": [1, 2]}
print(f"{user['full_name'].title()}") # Output: Alice Cooper
print(f"{len(user['accounts'])} accounts") # Output: 2 accounts
print(f"{user['full_name'].split()[0]}") # Output: alice
All legal, and all fine in moderation. The limit is readability: once the expression needs a second look, compute it on the line above and put the name in the braces. An f-string is for presenting a value, not for calculating one.
What 3.12 changed
Before 3.12 the quote inside the braces had to differ from the quote around the string, so you were
forever alternating " and '. 3.12 rebuilt f-strings on the normal parser and
that restriction went away:
user = {"full_name": "Alice Cooper"}
print(f"{user["full_name"]}") # Output: Alice Cooper
Same quote inside and out. That is a SyntaxError on 3.11 and earlier, so it is worth
knowing which is which if you are contributing to an older project. Nesting is unlimited now too, and
backslashes are allowed inside the braces.
Template strings, new in 3.14
3.14 added t"...", which looks like an f-string but does not produce a
string. It produces a Template object holding the literal parts and the values
separately:
name = "Alice"
template = t"Hello, {name}"
print(type(template).__name__) # Output: Template
print(template.strings) # Output: ('Hello, ', '')
print(template.values) # Output: ('Alice',)
The point is safety. Because the values are kept apart from the literal text, library code can escape them correctly before assembling — which is how you get SQL and HTML interpolation that cannot be injected into. You will meet this in libraries before you write it yourself, and it needs 3.14.
Multi-line, and literal braces
For anything longer than a line, triple quotes keep the layout of the output visible in the source:
name, total = "Alice", 9650.5
receipt = f"""Customer: {name}
Total: ${total:,.2f}"""
print(receipt)
# Output: Customer: Alice
# Output: Total: $9,650.50
Every line of a triple-quoted string is taken literally, indentation included — so a receipt
written inside an indented function will carry that indentation into the output. Either write it at
the left margin or run it through textwrap.dedent().
To print an actual brace, double it:
field = "name"
print(f"{{{field}}}") # Output: {name}
print(f"{{ literal }}") # Output: { literal }
Three braces in a row reads badly and is the one genuinely ugly corner of the syntax:
the outer pair is the literal, the inner one is the expression. It comes up when you generate JSON or
CSS by hand — at which point the honest answer is to use the json module instead.
The older styles
You will meet both. Read them, do not write them.
name, qty = "Alice", 3
print("Hi %s, you have %d" % (name, qty)) # Output: Hi Alice, you have 3
print("Hi {}, you have {}".format(name, qty)) # Output: Hi Alice, you have 3
print(f"Hi {name}, you have {qty}") # Output: Hi Alice, you have 3
% is inherited from C and is still everywhere in old code. .format()
replaced it in Python 3 and takes the same format spec f-strings do. The f-string says the same thing
with the values in the sentence.
One place still uses the old style on purpose: the logging module wants
logger.info("Saved %s", name) rather than an f-string, so it can skip building the
message entirely when that log level is switched off. Debugging
covers it.
Next: Conditional Statements.