Print 2 Decimal Places in Python (.2f, round and Decimal)

Printing a number to 2 decimal places in Python is a job for an f-string and the .2f format spec:

price = 3.14159
print(f"{price:.2f}")        # 3.14

That gives you a string. round(price, 2) gives you a number instead, and the two behave differently often enough to be worth separating properly.

Below: what .2f actually means, why round(2.5) is 2, and what to use for money. Output is from Python 3.12.5.

Diagram comparing Python f-string formatting with the round function for two decimal places
Formatting gives text and always pads; rounding gives a number and does not.

Print a float with 2 decimal places

Three ways to write it, all producing the same text:

price = 3.14159

print("f-string   ->", f"{price:.2f}")
print("format()   ->", format(price, ".2f"))
print("% operator ->", "%.2f" % price)
print()
print("all three give the same text. The f-string is the modern one.")
print()

print("used inside a sentence:")
print(f"  the total is {price:.2f} dollars")

Output:

f-string   -> 3.14
format()   -> 3.14
% operator -> 3.14

all three give the same text. The f-string is the modern one.

used inside a sentence:
  the total is 3.14 dollars
Command Prompt showing a Python float printed to two decimal places with an f-string format and the percent operator
The f-string, format() and % all produce the same result.

The f-string is the current style and reads best inside a sentence. The % form still appears in older code and in logging calls, where it has a genuine advantage.

What .2f means in Python

The spec is three separate pieces, which is worth seeing spelled out:

value = 1234.5678

print("reading the format spec  .2f  one piece at a time:")
print()
print("  f   means fixed-point: always show the decimal part")
print("  2   means how many digits after the point")
print("  .   separates the number from the kind of formatting")
print()
print("so changing the 2 changes the digits:")
for places in (0, 1, 2, 4):
    print(f"  .{places}f -> {value:.{places}f}")
print()
print("and f can be swapped for another kind:")
print(f"  .2f -> {value:.2f}      fixed point")
print(f"  .2e -> {value:.2e}   scientific")
print(f"  .2% -> {0.1234:.2%}       percentage")

Output:

reading the format spec  .2f  one piece at a time:

  f   means fixed-point: always show the decimal part
  2   means how many digits after the point
  .   separates the number from the kind of formatting

so changing the 2 changes the digits:
  .0f -> 1235
  .1f -> 1234.6
  .2f -> 1234.57
  .4f -> 1234.5678

and f can be swapped for another kind:
  .2f -> 1234.57      fixed point
  .2e -> 1.23e+03   scientific
  .2% -> 12.34%       percentage

f asks for fixed-point notation, meaning a plain decimal number with no exponent. The 2 sets how many digits follow the point. The dot separates them.

Swapping the f changes the style entirely: e gives scientific notation and % multiplies by 100 and adds a percent sign.

Python f-string 2 decimal places versus round()

They look interchangeable and are not:

price = 3.14159

formatted = f"{price:.2f}"
rounded = round(price, 2)

print("f-string ->", formatted, "  type:", type(formatted).__name__)
print("round()  ->", rounded, "  type:", type(rounded).__name__)
print()
print("one gives you TEXT for display, the other gives a NUMBER")
print("you can keep calculating with.")
print()

print("and they disagree about trailing zeros:")
print("  f'{5:.2f}'    ->", f"{5:.2f}")
print("  round(5, 2)   ->", round(5, 2))
print("  f'{2.5:.2f}'  ->", f"{2.5:.2f}")
print("  round(2.5, 2) ->", round(2.5, 2))
print()
print("formatting pads to exactly two places. round() just rounds,")
print("so a number with nothing to show loses the zeros.")

Output:

f-string -> 3.14   type: str
round()  -> 3.14   type: float

one gives you TEXT for display, the other gives a NUMBER
you can keep calculating with.

and they disagree about trailing zeros:
  f'{5:.2f}'    -> 5.00
  round(5, 2)   -> 5
  f'{2.5:.2f}'  -> 2.50
  round(2.5, 2) -> 2.5

formatting pads to exactly two places. round() just rounds,
so a number with nothing to show loses the zeros.
Command Prompt comparing Python f-string formatting with round showing string versus float results
Formatting returns a string and pads; round() returns a number and does not.

f"{price:.2f}" produces text for display. round(price, 2) produces a number you can keep calculating with. Using the wrong one gives you a string where you wanted arithmetic, or a bare 5 where you wanted 5.00.

The padding difference is the visible one. Formatting always shows exactly two places; rounding drops trailing zeros because a float has no concept of them.

Keep the full-precision number in your variables and format only at the point of display. Rounding early loses information you may need. Converting text back to a number is the other half of that round trip.

Why Python round(2.5) gives 2 instead of 3

This is the result people report as a bug:

print("Python does NOT round a tie upwards. It rounds to the nearest")
print("EVEN number, which is called banker's rounding:")
print()
for value in (0.5, 1.5, 2.5, 3.5, 4.5, 5.5):
    print(f"  round({value}) -> {round(value)}")
print()
print("0.5 goes down to 0 and 1.5 goes up to 2, because 0 and 2 are even.")
print()
print("it exists so that rounding a long column of numbers does not")
print("drift upwards, which repeated round-half-up would cause.")
print()
print("f-strings do the same thing:")
for value in (0.5, 1.5, 2.5):
    print(f"  f'{{{value}:.0f}}' -> {value:.0f}")

Output:

Python does NOT round a tie upwards. It rounds to the nearest
EVEN number, which is called banker's rounding:

  round(0.5) -> 0
  round(1.5) -> 2
  round(2.5) -> 2
  round(3.5) -> 4
  round(4.5) -> 4
  round(5.5) -> 6

0.5 goes down to 0 and 1.5 goes up to 2, because 0 and 2 are even.

it exists so that rounding a long column of numbers does not
drift upwards, which repeated round-half-up would cause.

f-strings do the same thing:
  f'{0.5:.0f}' -> 0
  f'{1.5:.0f}' -> 2
  f'{2.5:.0f}' -> 2
Command Prompt showing Python round using bankers rounding where ties go to the nearest even number
Ties round to the nearest even number, so 0.5 goes down and 1.5 goes up.

Python rounds a tie to the nearest even number rather than always upwards. 0.5 becomes 0, 1.5 becomes 2, and 2.5 becomes 2.

It is deliberate. Always rounding halves upwards biases a long column of numbers slightly high, and rounding to even cancels that out across many values.

f-strings follow the same rule, so switching to formatting does not change the behaviour.

Why Python round(2.675, 2) gives 2.67

A different cause, with the same flavour of surprise:

from decimal import Decimal

print("this one looks like a bug and is not:")
print("  round(2.675, 2) ->", round(2.675, 2), " expected 2.68")
print()
print("the reason is that 2.675 is not really 2.675:")
print(" ", Decimal(2.675))
print()
print("it is stored slightly BELOW 2.675, so rounding down is correct.")
print()

print("the same thing with another value:")
print("  round(1.005, 2) ->", round(1.005, 2))
print(" ", Decimal(1.005))
print()
print("and the classic:")
print("  0.1 + 0.2 ->", 0.1 + 0.2)
print("  f'{0.1 + 0.2:.2f}' ->", f"{0.1 + 0.2:.2f}", " formatting hides it")

Output:

this one looks like a bug and is not:
  round(2.675, 2) -> 2.67  expected 2.68

the reason is that 2.675 is not really 2.675:
  2.67499999999999982236431605997495353221893310546875

it is stored slightly BELOW 2.675, so rounding down is correct.

the same thing with another value:
  round(1.005, 2) -> 1.0
  1.00499999999999989341858963598497211933135986328125

and the classic:
  0.1 + 0.2 -> 0.30000000000000004
  f'{0.1 + 0.2:.2f}' -> 0.30  formatting hides it
Command Prompt showing that Python stores 2.675 as slightly less than 2.675 so it rounds down
2.675 is stored just below 2.675, so rounding down is correct.

The number you typed is not the number stored. Binary floating point cannot represent 2.675 exactly, and the value it holds is very slightly smaller, so rounding down is the right answer for the value that actually exists.

Formatting hides this most of the time, which is why f"{0.1 + 0.2:.2f}" looks fine. The underlying value is unchanged. Checking for NaN covers another corner of float behaviour.

Using Decimal for money

When the rounding rule is part of the requirement, floats are the wrong type:

from decimal import Decimal, ROUND_HALF_UP

print("when the rounding rule matters, use Decimal with strings:")
print()
for text in ("2.675", "1.005", "2.5"):
    value = Decimal(text).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    print(f"  Decimal('{text}').quantize(Decimal('0.01'), ROUND_HALF_UP) -> {value}")
print()
print("note the quotes. Decimal('2.675') is exactly 2.675;")
print("Decimal(2.675) inherits the float's error:")
print(" ", Decimal(2.675))
print()

print("money arithmetic stays exact:")
total = Decimal("19.99") * 3
print("  Decimal('19.99') * 3 ->", total)
print("  as currency          ->", total.quantize(Decimal("0.01")))

Output:

when the rounding rule matters, use Decimal with strings:

  Decimal('2.675').quantize(Decimal('0.01'), ROUND_HALF_UP) -> 2.68
  Decimal('1.005').quantize(Decimal('0.01'), ROUND_HALF_UP) -> 1.01
  Decimal('2.5').quantize(Decimal('0.01'), ROUND_HALF_UP) -> 2.50

note the quotes. Decimal('2.675') is exactly 2.675;
Decimal(2.675) inherits the float's error:
  2.67499999999999982236431605997495353221893310546875

money arithmetic stays exact:
  Decimal('19.99') * 3 -> 59.97
  as currency          -> 59.97

Decimal stores numbers as written, so Decimal("2.675") really is 2.675 and ROUND_HALF_UP gives 2.68. The quotes matter: passing a float hands the error straight through.

quantize() sets the number of places and the rule together, which is what financial code needs. Currency arithmetic also stays exact rather than accumulating tiny errors.

Format numbers with thousands separators and alignment

The same spec hand