Python f-strings: Formatting Numbers, Dates and Debug Output
An f-string is a string with an f before the opening quote, and anything inside { } is evaluated as Python and inserted into the text. Add a colon to control the format: f"{marks:.2f}" gives two decimal places, f"{amount:,}" adds thousands separators, and f"{ratio:.1%}" prints a percentage.
name = "Asha"
marks = 87.456
print(f"{name} scored {marks:.2f} marks")
# Asha scored 87.46 marks
That one line covers most of what people search for. The rest of this post walks through the format spec piece by piece, so you can read any f-string you meet in someone else's code and write your own without looking it up. Every example below is runnable; paste it into the browser Python terminal and change the numbers.
f-string basics: any expression goes inside the braces
The braces are not limited to variable names. You can put arithmetic, method calls, indexing, function calls and even a conditional expression inside them.
price = 499
qty = 3
items = ["pen", "notebook", "bag"]
print(f"Total: {price * qty}")
print(f"First item: {items[0].upper()}")
print(f"Count: {len(items)}, in stock: {'yes' if qty > 0 else 'no'}")
# Total: 1497
# First item: PEN
# Count: 3, in stock: yes
The expression is evaluated at the moment the line runs, using whatever values the variables hold at that point. If you need a literal brace in the output, double it: f"{{x}}" prints {x}.
If strings are still new to you, the free Strings lesson covers slicing, methods and f-strings with graded exercises you can run in the browser. It is one of the first 5 lessons, which are free.
The format spec: what comes after the colon
Everything after a colon inside the braces is the format spec. It follows a fixed order, and you rarely need more than two or three parts at once:
[fill][align][width][,][.precision][type]
- fill is any character used for padding (default is a space)
- align is
<left,>right,^centre - width is the minimum total width
- , or _ adds a thousands separator
- .precision is digits after the decimal point for floats
- type is
f(fixed),%(percent),d(integer),b,x,eand a few others
Width and alignment: printing clean tables
Width and alignment are how you line up columns in console output, which is handy for reports and for competitive-programming style output checks.
rows = [("Asha", 92), ("Rohit", 7), ("Meenakshi", 100)]
for name, score in rows:
print(f"|{name:<10}|{score:>5}|{score:^7}|")
# |Asha | 92| 92 |
# |Rohit | 7| 7 |
# |Meenakshi | 100| 100 |
Strings are left-aligned by default and numbers are right-aligned by default, so you only need < or > when you want the opposite. A fill character goes before the alignment sign, and a leading zero pads integers with zeros:
print(f"{7:03d}") # 007
print(f"{'hi':*^8}") # ***hi***
Zero padding is the usual way to build roll numbers or invoice IDs such as INV-007.
Numbers: decimals, thousands separators and percentages
These are the format specs people look up most often.
amount = 1234567.891
ratio = 0.8734
print(f"{amount:.2f}") # 1234567.89
print(f"{amount:,.2f}") # 1,234,567.89
print(f"{amount:_.0f}") # 1_234_568
print(f"{ratio:.1%}") # 87.3%
print(f"{255:b} {255:x} {255:#x}") # 11111111 ff 0xff
A few details worth knowing:
.2frounds, it does not cut off.87.456becomes87.46.%multiplies by 100 for you, so pass the fraction (0.8734), not the percentage.- The
,separator always groups in threes (1,234,567). It does not produce the Indian lakh and crore grouping (12,34,567); for that you need your own helper or a locale-aware library. - A dollar sign before the braces is just text:
f"FOB: ${usd:.2f}"withusd = 19.5printsFOB: $19.50.
usd = 19.5
print(f"FOB: ${usd:.2f}") # FOB: $19.50
print(f"MRP: ₹{1499:,}") # MRP: ₹1,499
The Numbers and Math lesson goes further into integer division, rounding and float precision, which explains why .2f is a display choice and not a fix for floating-point error.
Dates in f-strings
Date and datetime objects accept strftime codes directly as the format spec, so you do not need to call .strftime() separately.
from datetime import datetime
d = datetime(2026, 9, 24, 14, 5)
print(f"{d:%d/%m/%Y}") # 24/09/2026
print(f"{d:%d %b %Y, %I:%M %p}") # 24 Sep 2026, 02:05 PM
print(f"{d:%A}") # Thursday
%d/%m/%Y is the DD/MM/YYYY order used in India. %b, %A and %p depend on the system locale; on a default English setup they print as shown. The Dates and times lesson covers parsing, time zones and date arithmetic.
The = specifier for quick debugging
Since Python 3.8, adding = after an expression prints the expression text along with its value. It is the fastest way to inspect variables without typing each name twice.
x = 42
marks = 87.456
name = "Asha"
items = ["a", "b"]
print(f"{x=}") # x=42
print(f"{len(items)=}") # len(items)=2
print(f"{x * 2 = }") # x * 2 = 84
print(f"{marks=:.1f}") # marks=87.5
print(f"{name=}") # name='Asha'
Two things to notice. Spaces around = are kept in the output, so {x * 2 = } prints x * 2 = 84. And with =, strings are shown with quotes because the value is displayed using repr(). You can ask for that explicitly anywhere with the !r conversion: f"{name!r}" gives 'Asha'.
Quotes and backslashes inside f-strings (Python 3.12 rules)
Before Python 3.12, you could not reuse the outer quote character inside the braces, and backslashes were not allowed in the expression part. Python 3.12 (PEP 701) removed both restrictions.
student = {"name": "Asha", "city": "Pune"}
print(f"{student["name"]} lives in {student["city"]}")
# Asha lives in Pune
lines = ["one", "two", "three"]
print(f"Items:\n{'\n'.join(lines)}")
# Items:
# one
# two
# three
Both lines above are a SyntaxError on Python 3.11 and earlier. If your code has to run on older versions, keep using the other quote type inside the braces (f"{student['name']}") and move any backslash expression into a variable first. Many college lab machines and older servers still run 3.10 or 3.11, so it is worth knowing both styles.
When to use str.format or % instead
f-strings are the default choice for new code, but they are evaluated immediately, where they are written. That makes them the wrong tool in a few cases.
Templates stored separately from the data. If the text lives in a config file or a constant and is filled in later, use str.format:
template = "Dear {name}, your order {order_id} has shipped."
print(template.format(name="Asha", order_id="A-1042"))
# Dear Asha, your order A-1042 has shipped.
Logging. The logging module uses %-style placeholders and only builds the message if the log level is enabled. Pass the values as arguments instead of pre-formatting them:
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
logging.info("Loaded %d rows from %s", 1200, "sales.csv")
# INFO Loaded 1200 rows from sales.csv (written to stderr)
SQL queries. Never build SQL with an f-string. Use the database driver's parameter placeholders (? in sqlite3) so values are escaped properly.
The old "%.2f" % value style still works and you will see it in older code, but there is no reason to choose it for new code outside logging.
Quick reference
| You want | Write | Result |
|---|---|---|
| 2 decimals | f"{3.14159:.2f}" | 3.14 |
| Thousands separator | f"{1234567:,}" | 1,234,567 |
| Percent | f"{0.256:.1%}" | 25.6% |
| Zero pad | f"{42:05d}" | 00042 |
| Right align in 8 | f"{'ok':>8}" | ok |
| Date | f"{d:%d/%m/%Y}" | 24/09/2026 |
| Debug | f"{x=}" | x=42 |
Practise it
Reading format specs is easy; remembering them under exam or interview pressure is not. Open the practice editor and try this: given a list of (name, marks) tuples, print a table with names left-aligned in 12 characters, marks right-aligned to two decimals, and a final line showing the class average as a percentage of 100. Then work through the graded exercises in the Strings lesson, which run real Python in your browser with no install.