Backend / Python core / Tricky questions / 17_floats_and_equality.md

Floats and equality

Updated 2 min read source
On this page5
  1. The gotcha
  2. Minimal repro
  3. Why it happens
  4. How to avoid
  5. Interview angle

Floats and equality

The gotcha

0.1 + 0.2 != 0.3. IEEE 754 double-precision floats can’t represent most decimal fractions exactly. Equality comparison fails for values that “should” be equal.

Minimal repro

python
0.1 + 0.2          # 0.30000000000000004
0.1 + 0.2 == 0.3   # False

# Underlying:
from decimal import Decimal
Decimal(0.1)
# Decimal('0.1000000000000000055511151231257827021181583404541015625')

The literal 0.1 is not the rational 1/10 — it’s the nearest representable double. Same for 0.2 and 0.3. The closest doubles to 0.1 and 0.2 add up to a number whose closest double is not the closest double to 0.3.

Why it happens

Floats use a binary radix. 0.1 decimal has no terminating binary representation (like 1/3 has no terminating decimal). The runtime stores 53 bits of mantissa, which is the closest possible — but rounding errors accumulate across operations.

Special values to know:

  • float("inf"), float("-inf"), float("nan") — finite arithmetic doesn’t always behave: nan != nan is True.
  • 0.0 == -0.0 is True but 1/0.0 raises and 1/-0.0 raises (in Python; in C they’d be ±inf).
  • True / 0 raises ZeroDivisionError.

How to avoid

For tolerant equality:

python
import math
math.isclose(0.1 + 0.2, 0.3)              # True
math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)

For exact decimal arithmetic (money, etc.), use Decimal:

python
from decimal import Decimal, getcontext
# True
Decimal("0.1") + Decimal("0.2") == Decimal("0.3")

Pass strings to Decimal, not floats — Decimal(0.1) inherits the float’s imprecision.

Traps that survive Decimal

Two traps that outlive the switch to Decimal:

python
from decimal import Decimal, ROUND_HALF_UP

int(19.99 * 100)                           # 1998: a cent lost converting to cents
round(2.5), round(3.5)                     # (2, 4): banker's rounding
Decimal("2.665").quantize(Decimal("0.01")) # Decimal('2.66'): half-even again
Decimal("2.665").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)  # '2.67'

Both round() and the default Decimal context round half to even; invoices and tax usually need half-up, so pass rounding= explicitly. And the classic “sum a tenth ten times” demo no longer shows drift through sum(): since Python 3.12, sum() of floats uses compensated summation and returns 1.0; a running total += 0.1 still gives 0.9999999999999999. Money handling in full: The double-entry ledger.

For NaN-safety:

python
import math
# only correct way to test for NaN
math.isnan(x)

Interview angle

“What does 0.1 + 0.2 == 0.3 evaluate to, and why?” Follow-up: “How would you compare two floats safely?” Bonus: “Is nan == nan True?” (No — IEEE 754 says NaN is not equal to anything, including itself.)