Python f-strings Explained: Formatting Numbers, Dates and Debug Output
f-strings are the clearest way to build text in Python. Learn the format codes for money, percentages, padding and dates, the handy debug shortcut, and the traps worth knowing, with every example run.
- An f-string is a string with an
fprefix, where anything in{}is evaluated as Python code. - After a colon, a format specifier controls decimals, separators, width, alignment and dates.
{value=}prints both the expression and its value, which is ideal for quick debugging.- Do not use f-strings to build SQL queries or to format untrusted templates.
Building a line of text from variables used to mean a tangle of + signs and str() calls. Since Python 3.6, f-strings let you write the text as it should look and drop the values straight into it. They are short, readable and, in our timing test, among the fastest options.
Every example below was run with Python 3.13, and the output shown is what we got.
What is an f-string?
An f-string, or formatted string literal, is a normal string with the letter f (or F) in front. Inside it, curly braces mark expressions. Python evaluates each one when the line runs and inserts the result as text.
The expressions are real Python, so you can do arithmetic, call functions, index lists or read dictionary keys directly inside the braces.
Basic syntax
name, price, qty = "Asha", 1234.5, 3
print(f"{name} bought {qty} items for {price * qty}")
# Asha bought 3 items for 3703.5
items = {"pen": 10}
print(f"{items['pen']}")
# 10
Notice the inner quotes in the second example. The f-string uses double quotes, so the dictionary key uses single quotes. Since Python 3.12 you can even reuse the same quote type inside the braces, but mixing them is still easier to read and works on older versions.
The format specifier
Add a colon after the expression to control how it is shown. The part after the colon follows Python’s format mini-language, which has this general shape:
{value:[fill][align][sign][width][grouping][.precision][type]}
You rarely need all of it. Most real formatting uses one or two pieces, such as :.2f for two decimal places or :>10 to right-align in ten characters. The same mini-language is used by str.format() and the built-in format() function, so what you learn here works there too.
Formatting numbers
price = 1234.5
print(f"{price:.2f} | {price:,.2f}")
# 1234.50 | 1,234.50
print(f"{0.4567:.1%} | {42:05d} | {255:x} {255:X} {255:o} {255:b} | {255:#x}")
# 45.7% | 00042 | ff FF 377 11111111 | 0xff
print(f"{1234567:_} | {-5:+d} {5:+d}")
# 1_234_567 | -5 +5
print(f"{1/3:.3g} {123456789:.3g} {0.000012345:.3g}")
# 0.333 1.23e+08 1.23e-05
| Specifier | Meaning | Example result |
|---|---|---|
.2f |
Fixed point, 2 decimals | 1234.50 |
,.2f |
Thousands separator | 1,234.50 |
_ |
Underscore separator | 1_234_567 |
.1% |
Percentage, multiplied by 100 | 45.7% |
05d |
Integer padded with zeros | 00042 |
+d |
Always show the sign | +5 |
x, o, b |
Hex, octal, binary | ff, 377, 11111111 |
.3g |
Significant figures, switches to scientific when needed | 1.23e+08 |
The comma separator always uses commas in groups of three. For Indian-style grouping, such as 12,34,567, or for locale-specific currency, use the locale module or a library such as Babel rather than f-strings alone.
Alignment and padding
print(f"{price:>12.2f}|{price:<12.2f}|{price:^12.2f}|")
# 1234.50|1234.50 | 1234.50 |
print(f"[{name:*^10}]")
# [***Asha***]
width = 8
print(f"[{price:{width}.1f}]")
# [ 1234.5]
> aligns right, < aligns left and ^ centres. A character before the arrow, like *, sets the fill. The last example shows a nested field: the width itself comes from a variable, which is handy when building simple text tables whose column sizes depend on the data.
Numbers align right by default and strings align left, which is usually what you want in reports.
Dates and times
Datetime objects accept the same codes as strftime directly after the colon:
import datetime
d = datetime.datetime(2026, 11, 8, 14, 5)
print(f"{d:%d %b %Y, %H:%M} | {d:%A}")
# 08 Nov 2026, 14:05 | Sunday
Common codes include %Y for the four-digit year, %m for the month number, %d for the day, %H:%M for 24-hour time and %A for the weekday name. For machine-readable output, such as logs and APIs, prefer d.isoformat(), which produces a standard format other systems can parse.
The = debug shortcut
Add = after an expression, and Python prints the expression text as well as its value:
print(f"{name=} {price*qty=}")
# name='Asha' price*qty=3703.5
This arrived in Python 3.8 and is a big time-saver when you are tracing a bug. You can still add a format specifier after it, as in f"{price=:.2f}". Notice that the string value is shown with quotes. The = form uses repr() by default, which makes stray spaces and empty strings easy to spot.
You can choose the conversion yourself with !r for repr() and !s for str():
print(f"{name!r} {name!s}")
# 'Asha' Asha
For anything beyond a quick check, use the logging module, which our exception handling guide also recommends for recording errors.
Rounding surprises
This one confuses many people:
import decimal
print(f"{2.675:.2f} vs {decimal.Decimal('2.675'):.2f}")
# 2.67 vs 2.68
The float did not round up. That is not an f-string bug. The number 2.675 cannot be stored exactly in binary floating point, so the value Python holds is very slightly below 2.675, and it rounds down. The Decimal version holds the exact value and rounds as you would expect on paper.
Tip: For money, store and calculate amounts with decimal.Decimal or as whole numbers of the smallest unit, such as paise or cents. Use f-strings only for the final display.
Quotes, braces and backslashes
To show a literal brace, double it:
print(f"{{literal braces}} {qty}")
# {literal braces} 3
Before Python 3.12, a backslash was not allowed inside the braces, so code like f"{'\n'.join(lines)}" was a syntax error and people assigned the joined text to a variable first. Python 3.12 relaxed these rules, and our example ran fine on 3.13. If your code must support older versions, keep backslashes and repeated quote types out of the braces.
Also remember that the f prefix is what makes the magic happen. A plain string with braces is just text:
user = "x"
template = "Hello {user}"
print(template)
# Hello {user}
f-strings vs format() vs %
We timed one million runs of the same short message with each style using timeit:
f-string 0.129 s
format() 0.241 s
percent 0.135 s
On this machine the f-string was fastest, with the old % style close behind and str.format() slowest. The differences are tiny per call, so choose for readability first.
| Style | Example | Best for |
|---|---|---|
| f-string | f"{n} has {q}" |
Almost all new code |
str.format() |
"{} has {}".format(n, q) |
Templates defined before the values exist |
% formatting |
"%s has %s" % (n, q) |
Logging calls and older code |
string.Template |
Template("$n has $q") |
Simple templates written by users |
One place where % style still wins is logging. Writing logger.debug("user %s", name) lets the logging module skip building the message entirely when debug output is switched off, whereas an f-string is always evaluated.
When not to use f-strings
- SQL queries. Never insert values into SQL with an f-string. It opens the door to SQL injection. Use parameters, such as
cursor.execute("SELECT * FROM users WHERE name = ?", (name,)). Our SQL tutorial shows the safe pattern. - Shell commands. Build argument lists for
subprocessinstead of formatting a command string. - User-supplied templates. f-strings are evaluated in your code, so they cannot be loaded from a file or database. Formatting user-written templates with
str.format()can also expose object attributes, so usestring.Templateor a proper template engine. - Translations. Text that needs translating usually goes through a function such as
gettextfirst, with placeholders filled afterwards.
Common mistakes
- Forgetting the f. The braces appear literally in the output.
- Using
:.2fon a string. You getValueError: Unknown format code 'f'. Convert withfloat()first. - Putting heavy logic in the braces. If an expression needs explaining, compute it on the line before.
- Expecting
.2fto fix float precision. It changes only how the number is displayed.
Putting it together: a small text report
Alignment, separators and decimals combine nicely for console reports and log summaries:
rows = [("Pen", 3, 10.0), ("Notebook", 12, 45.5), ("Desk lamp", 1, 1299.0)]
print(f"{'Item':<12}{'Qty':>5}{'Price':>10}")
for name, qty, price in rows:
print(f"{name:<12}{qty:>5}{price:>10,.2f}")
total = sum(q * p for _, q, p in rows)
print(f"{'Total':<12}{'':>5}{total:>10,.2f}")
# Item Qty Price
# Pen 3 10.00
# Notebook 12 45.50
# Desk lamp 1 1,299.00
# Total 1,875.00
The column headers are plain strings inside the braces, formatted with the same widths as the data, so everything lines up. Triple-quoted f-strings work too, and an inline conditional keeps grammar correct:
user, n = "Asha", 3
msg = f"""Hi {user},
You have {n} new {'message' if n == 1 else 'messages'}."""
# Hi Asha,
# You have 3 new messages.
For anything wider or more complex, such as HTML emails or documents, move to a template engine like Jinja, which keeps layout and data apart.
Final thoughts
f-strings make formatted output readable at a glance. Learn a handful of specifiers, .2f, ,, %, alignment arrows and date codes, plus the = debug shortcut, and you will cover nearly every everyday need. Keep them for display text, and use parameters for SQL and proper templates for anything users can edit.
Frequently asked questions
What is an f-string in Python?
It is a string literal prefixed with f, where expressions inside curly braces are evaluated and inserted as text. f-strings were added in Python 3.6.
How do I format a float to 2 decimal places with an f-string?
Use the .2f specifier, as in f”{price:.2f}”. Add a comma, as in f”{price:,.2f}”, to include a thousands separator.
What does = do inside an f-string?
It prints the expression and its value together, such as name=’Asha’. It was added in Python 3.8 and is useful for quick debugging.
Are f-strings faster than format()?
Usually, yes. In our timing of one million calls, f-strings took 0.129 seconds against 0.241 seconds for str.format(). Choose for readability first, since both are fast.
Can I use f-strings for SQL queries?
No. Formatting values into SQL text allows SQL injection. Pass values as query parameters instead.
Keep learning
Need help with testing, WordPress or SEO?
We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.