Python f-string Number Formatting
You format a number in a Python f-string by adding a format spec after a colon inside the braces, such as f"{price:.2f}" for two decimal places. The spec also controls thousands separators, percentages, width, padding, and sign, and its parts always go in this order: f"{value:[fill][align][sign][#][0][width][,][.precision][type]}".
Quick answer: Put the format spec after a colon. :.2f gives two decimal places (f"{3.14159:.2f}" gives '3.14'), :,.2f adds comma thousands separators (f"{1234567.891:,.2f}" gives '1,234,567.89'), :.1% multiplies by 100 and adds a percent sign (f"{0.0857:.1%}" gives '8.6%'), and :05d zero-pads an integer to width 5 (f"{42:05d}" gives '00042'). Values are rounded, not truncated, and the result is always a string.
How do you format decimal places in a Python f-string?
Put .Nf after the colon, where N is the number of digits after the decimal point: :.2f gives two decimal places and :.0f gives none. The value is rounded and the result is a string.
pi = 3.14159265359
f"{pi:.2f}" # '3.14'
f"{pi:.4f}" # '3.1416'
f"{pi:.0f}" # '3' (rounded to integer, still a string)
# Round-half-to-even (banker's rounding) is used:
f"{2.5:.0f}" # '2'
f"{3.5:.0f}" # '4'
Should you use f or d to format floats and integers in an f-string?
Use f for floats and d for integers: f is fixed-point and also accepts ints, while d is base-10 integer formatting and raises ValueError on a float. A third type letter, g, chooses between fixed-point and scientific notation for you.
# f = fixed-point float
f"{1234.5:.2f}" # '1234.50'
# d = integer (errors on a float)
f"{1234:d}" # '1234'
# f"{1234.5:d}" -> ValueError
# g = general (drops trailing zeros, switches to scientific for big/small)
f"{1234.5:g}" # '1234.5'
f"{0.000012345:g}" # '1.2345e-05'
How do you add comma thousands separators in an f-string?
Add a comma (or underscore) before the precision to group digits in threes: :,.0f gives commas with no decimals, and :,.2f gives commas with two decimal places.
amount = 1234567.891
f"{amount:,}" # '1,234,567.891'
f"{amount:,.2f}" # '1,234,567.89'
f"{amount:,.0f}" # '1,234,568' (commas, no decimals)
f"{amount:_.2f}" # '1_234_567.89' (PEP 515 underscore separator)
# Locale-aware (uses LC_NUMERIC) โ useful for European-style 1.234.567,89:
f"{amount:n}" # depends on locale.setlocale()
How to Format Percentages
The % type multiplies the value by 100, formats it like f, and appends a percent sign, so :.2% means two decimal places after scaling.
ratio = 0.0857
f"{ratio:.0%}" # '9%' (0.0857 -> 8.57 -> rounds to 9)
f"{ratio:.1%}" # '8.6%'
f"{ratio:.2%}" # '8.57%'
# % multiplies by 100, then rounds exactly like f (exact ties go to even):
f"{0.125:.0%}" # '12%' (12.5 -> 12)
f"{0.375:.0%}" # '38%' (37.5 -> 38)
# Most "wrong" results are binary floats, not ties. 0.01005 * 100 is
# stored as 1.00499999..., so it rounds down:
f"{0.01005:.2%}" # '1.00%' (not '1.01%')
# Already a percent? Use f and append the symbol manually:
pct = 8.57
f"{pct:.2f}%" # '8.57%'
Width, Padding, and Alignment
A number before the precision or type sets the minimum width. Numbers right-align by default; <, ^, and > change the alignment, and a 0 in front of the width pads with zeros.
n = 42
f"{n:5d}" # ' 42' (width 5, right-aligned by default for numbers)
f"{n:<5d}" # '42 ' (left-align)
f"{n:^5d}" # ' 42 ' (center)
f"{n:05d}" # '00042' (zero-pad)
f"{n:>5d}" # ' 42' (right-align, explicit)
# Custom fill character:
f"{n:*^7d}" # '**42***'
# Combine with thousands and decimals:
f"{1234.5:>12,.2f}" # ' 1,234.50'
Forcing the Sign
The sign option controls what appears in front of positive numbers: + always shows a sign, a space reserves a column for it, and - (the default) shows only minus signs.
x = 42
y = -42
f"{x:+d}" # '+42' (always show sign)
f"{y:+d}" # '-42'
f"{x: d}" # ' 42' (space for positives, '-' for negatives โ aligns columns)
f"{x:-d}" # '42' (default: only show '-')
Scientific Notation and Hex/Binary
The e and E types give scientific notation at the precision you set. For integers, b, o, x, and X change the base, and # adds the base prefix.
tiny = 0.000001234
f"{tiny:.2e}" # '1.23e-06'
f"{tiny:.2E}" # '1.23E-06'
# Integer bases:
f"{255:b}" # '11111111' binary
f"{255:o}" # '377' octal
f"{255:x}" # 'ff' hex (lowercase)
f"{255:X}" # 'FF' hex (uppercase)
f"{255:#x}" # '0xff' with prefix
The Format Spec, in Order
Every piece is optional, but they must appear in this order after the colon:
{value:[fill][align][sign][#][0][width][,][.precision][type]}
# Worked example โ fill='0', align='>', width=8, comma, .2f:
f"{1234.5:0>8,.2f}" # '1,234.50' (already 8 chars, no fill added)
f"{12.5:0>8,.2f}" # '00012.50'
Common Pitfalls
Most f-string number bugs come from type mismatches, rounding, units, and width.
- Mixing types:
f"{1234:.2f}"works (Python coerces int to float), butf"{1234.5:d}"raisesValueError. - Rounding surprises: Most unexpected results come from binary floats, not exact ties:
0.575is stored as slightly less than 0.575, sof"{0.575:.0%}"gives'57%', not'58%'. Banker's rounding (round-half-to-even) only decides values that are exact ties in binary, like2.5. For accounting, usedecimal.DecimalwithROUND_HALF_UP. - Percent doubles the value:
:.0%multiplies by 100. Don't apply it to a number that's already in percent units. - Width includes the separator and decimal point:
f"{1234.5:8,.2f}"produces an 8-char string total, not 8 digits.
Pro Tip: Need a dynamic precision? You can nest a variable into the spec: f"{value:.{n}f}" formats with n decimal places, where n is computed at runtime.