Python – f-strings and Formatting

July 8, 20265 min readUpdated 8/20/2026

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.