f-strings in Python: formatting, specs, and the = trick
An f prefix lets you put expressions straight into a string. The format specs and the debugging form are what make them worth learning properly.
An f-string is a string with an f before the opening quote. Anything inside {} is evaluated and inserted:
name = "Ada"
age = 36
print(f"{name} is {age}")
# Ada is 36
The braces take expressions, not just variable names:
print(f"{age * 2}")
# 72
That is the whole basic idea. What makes f-strings worth learning past that point is the format specification after a colon, and one debugging shortcut that saves a surprising amount of time.
Every output on this page came from Python 3.13 through Pyodide 0.28.3.
Format specs
After a colon inside the braces, you can say how the value should be rendered:
pi = 3.14159
print(f"{pi:.2f}") # 3.14
print(f"{1234567:,}") # 1,234,567
print(f"{0.256:.1%}") # 25.6%
print(f"{42:05d}") # 00042
| Spec | Does | Example output |
|---|---|---|
.2f |
Fixed decimal places | 3.14 |
, |
Thousands separator | 1,234,567 |
.1% |
Percentage, one decimal | 25.6% |
05d |
Pad with zeros to width 5 | 00042 |
.3e |
Scientific notation | 3.142e+00 |
.2f is the one you will use most. It rounds for display without changing the underlying number, which is exactly what you want for money and measurements.
Alignment
Three characters control alignment, with a width after them:
print(f"{'left':<10}|{'right':>10}|{'mid':^10}|")
# left | right| mid |
< left, > right, ^ centre. This is how you produce aligned columns in terminal output without a table library. Add a fill character before the alignment marker to pad with something other than a space: {'x':*^9} centres inside asterisks.
The = trick
This is the feature most worth knowing and the one most often missed:
print(f"{name=}")
# name='Ada'
Adding = prints both the expression and its value. It works on any expression, not just names:
print(f"{age * 2 = }")
# age * 2 = 72
Spaces around the = are preserved in the output, so you can control the spacing.
This replaces the most common debugging line in Python. Instead of:
print("total_price:", total_price)
you write print(f"{total_price=}") and the label can never drift out of sync with the value, because there is only one place the name is written. On a line logging four variables, that matters.
Nesting values inside the spec
The format spec can itself contain braces, so widths and precisions can be computed:
w = 8
print(f"{pi:{w}.3f}")
# 3.142
Useful when the column width comes from the data, such as the longest name in a list.
Literal braces, and quotes
Two braces produce one:
print(f"{{literal braces}}")
# {literal braces}
Quotes inside the expression used to be a problem. Before Python 3.12 you could not reuse the same quote character that delimited the string, so this was a syntax error and people worked around it with alternating quote styles:
print(f"{items['key']}") # fine: different quote characters
Python 3.12 lifted the restriction, so the same quotes now nest. If you support older versions, keep alternating them.
Conversions and format hooks
!r gives you the repr rather than the string form:
print(f"{name!r}")
# 'Ada'
The quotes are visible, which is exactly what you want in debugging output, because it distinguishes Ada from "Ada " with a trailing space. !s forces str and !a forces ASCII.
Objects can define their own format specs. Dates are the everyday example:
from datetime import date
print(f"{date(2026, 9, 8):%Y-%m-%d}")
# 2026-09-08
The spec after the colon is passed to the date's own formatter, so any strftime pattern works there.
Conditionals inside braces
Any expression is allowed, including a conditional:
print(f"{'yes' if age > 18 else 'no'}")
# yes
This is legal and worth using sparingly. A short ternary reads fine; anything longer belongs on its own line above, because a complicated expression buried in a string is hard to read and harder to debug.
Rounding is not what you expect
Worth knowing before you format money:
print(f"{3.14159:.0f} {2.5:.0f} {3.5:.0f}")
# 3 2 4
2.5 became 2 and 3.5 became 4. Python rounds half values to the nearest even number, not always upward. This is round-half-to-even, and it is deliberate: always rounding up biases a long series of numbers slightly high, while rounding to even cancels out.
It surprises everyone the first time, and it means display rounding on financial figures can disagree with a spreadsheet. If you need the round-half-up behaviour, format through decimal with an explicit rounding mode rather than fighting the f-string.
Multiline and tables
Triple quotes work with the f prefix, and line breaks are preserved:
print(f"""multi
line {name}""")
Combining alignment with number formatting is how you produce a readable table:
print(f"{'Name':<10}{'Total':>10}")
print(f"{name:<10}{total:>10,.2f}")
Name Total
Ada 1,234.50
The spec >10,.2f stacks three things: right-align in ten characters, thousands separators, two decimals. They combine in that order, and reading them right to left (f float, .2 precision, , grouping, 10 width, > alignment) makes them easier to construct.
Number bases
Less common but occasionally exactly what you need:
n = 255
print(f"{n:b} {n:o} {n:x} {n:X} {n:#x}")
# 11111111 377 ff FF 0xff
Binary, octal, lowercase hex, uppercase hex, and the # prefix form that includes 0x. Useful for colours, permissions and byte-level work, and it saves a call to hex() plus string slicing.
When f-strings are the wrong tool
Two cases, and both matter.
Logging. Use the logging module's own placeholders instead:
logging.info("User %s failed login", user) # correct
logging.info(f"User {user} failed login") # avoid
The f-string builds the message immediately, even when the log level means it will be discarded. The %s form is only formatted if the message is actually emitted.
SQL queries. Never build a query with an f-string:
cur.execute(f"SELECT * FROM users WHERE name = '{name}'") # unsafe
cur.execute("SELECT * FROM users WHERE name = ?", (name,)) # correct
The first inserts raw text into the query, which is SQL injection. The second passes the value separately so the database treats it as data. This is the one place where the convenience of f-strings is actively dangerous.
The older forms
You will meet two other styles in existing code:
"%s is %d" % (name, age) # oldest
"{} is {}".format(name, age) # str.format
f"{name} is {age}" # f-string
All three work. f-strings are the current recommendation because the values sit where they are used rather than in a list at the end, which is easier to read and harder to misalign. .format() still has a use when the template and the values are genuinely separate, such as a message string loaded from a file, since an f-string is evaluated where it is written.
Three common mistakes
Forgetting the f. A plain string with braces prints the braces:
print("{name} is here") # {name} is here
No error, because that is a perfectly valid string. If your output contains literal braces and names, this is why. Editors with syntax highlighting colour the interpolated parts, which makes the missing prefix visible at a glance.
Putting a backslash inside the expression. Before Python 3.12 this was a syntax error:
f"{'\n'.join(items)}" # error on older versions
The fix that works everywhere is to compute it first:
joined = "\n".join(items)
print(f"{joined}")
That reads better anyway, and it is the general answer whenever an expression inside braces starts getting long.
Formatting a value that might be None. f"{value:.2f}" raises TypeError when value is None, and it does so at the point of display rather than where the None came from. Guard it or supply a default before formatting.
Recipes worth keeping
The specs combine, and a handful of combinations cover most real output:
f"${amount:,.2f}" # $1,234.50 money
f"{ratio:.1%}" # 25.6% percentage
f"{n:>8}" # right-aligned in a column
f"{name:<20}{count:>5}" # a two-column table row
f"{value:.3g}" # 3 significant figures
f"{when:%d %b %Y}" # 08 Sep 2026
f"{path!r}" # quoted, shows hidden whitespace
Two that come up constantly and are not obvious:
Plurals need a conditional rather than a spec:
f"{n} item{'s' if n != 1 else ''}"
Truncating long text uses precision on a string, which is easy to forget works:
f"{title:.20}" # first 20 characters
f"{title:<20.20}" # exactly 20 wide, padded or truncated
That second one is how you keep a column from breaking when a value is longer than expected.
Are they fast
Yes, and measurably faster than the alternatives, because the formatting is compiled into the bytecode rather than interpreted from a template string at runtime. % formatting and str.format both have to parse their template each time they run.
That is a reason to prefer f-strings, not a reason to rewrite working code. The difference is meaningful in a tight loop producing millions of strings, and invisible anywhere else.
The one place the speed argument reverses is logging, covered below: there, the fastest thing is not formatting at all.
Quick reference
f"{name}" # value
f"{a + b}" # expression
f"{pi:.2f}" # 2 decimal places
f"{n:,}" # thousands separator
f"{r:.1%}" # percentage
f"{n:05d}" # zero-padded
f"{s:<10}" # left align in width 10
f"{name=}" # name='Ada' (debugging)
f"{name!r}" # repr
f"{d:%Y-%m-%d}" # date formatting
f"{{}}" # literal braces
Ready to format real output? Start with the Python track, or read Python for loops for the loops these lines usually sit inside.
More from the blog
range() in Python: start, stop, step, and why it is lazy
range(1000000) uses 24 bytes. A list of a thousand numbers uses 4,028. Here is what range actually is, measured.
Read moreHow to convert a string to a number in JavaScript
Converting a string to a number in JavaScript: an empty string is 0 with Number and NaN with parseInt. What four methods do to awkward input.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.