Python f-string Number Formatting

โฑ๏ธ 3 min read ๐Ÿ Python

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.

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.

โ† Back to Python Tips