12 / 12

Why can floating-point arithmetic in Python produce results like `0.1 + 0.2 != 0.3`? How would you correctly compare floats?

Difficulty: 5/10
Operators & Expressions, Floating-Point Arithmetic, Float Comparison and Decimal

IEEE-754 representation error and safe float comparison

Python floats are IEEE-754 binary64 doubles: a sign bit, 11 exponent bits and 52 mantissa bits. Numbers like 0.1 and 0.2 have no finite binary representation (like 1/3 in decimal), so each is stored as the nearest representable double. Adding them rounds again, producing 0.30000000000000004, which is a different double from the nearest one to 0.3. Python is not buggy here; every language using doubles behaves identically. repr() shows the shortest string that round-trips, which is why the long tail appears.

javascript

How I compare floats depends on the domain. For general numeric code, math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0) (3.5+) is symmetric and scales with magnitude; always set abs_tol when either value may be near zero, because relative tolerance against 0 can never succeed. In tests I use pytest.approx. numpy.isclose has different semantics (asymmetric, default atol=1e-8), so do not assume the two agree. A hand-rolled abs(a - b) < 1e-9 is a common mistake because a fixed epsilon is too loose for tiny numbers and too strict for huge ones.

When not to use floats: money and anything requiring exact decimal results should use decimal.Decimal (constructed from strings, not floats - Decimal(0.1) captures the binary error) or integer minor units such as cents. For exact rational arithmetic use fractions.Fraction. For summing many floats, math.fsum or sorted/pairwise summation avoids accumulated error since float addition is not associative. Version note: since Python 3.12 the built-in sum() uses compensated summation for floats, so sum([0.1] * 10) returns 1.0 there but a plain += loop (or older versions) gives 0.9999999999999999.

Trade-offs: Decimal is exact for decimal fractions but 10-100x slower and still rounds on division; integers in cents are fast and exact but need care with percentages and rounding rules; floats are right for scientific and performance-critical work if you design for tolerance. Also note approximate equality is not transitive, so do not use it as a dictionary key or for deduplication without snapping values to a grid first.

Scenario Questions

0-2 years experience

  1. 1Why does adding 0.1 to a running total ten times in a loop not give exactly 1.0, and what value is actually produced?
  2. 2What does math.isclose do by default, and why is a tolerance needed at all?

2-5 years experience

  1. 1Your unit tests compare computed totals with expected floats and are flaky across machines. How do you make the assertions robust (pytest.approx, tolerances)?
  2. 2A shopping cart must be exact to the cent. Compare float, Decimal and integer cents, pick one, and explain the pitfall with Decimal(0.1).

5-8 years experience

  1. 1Summing millions of floats gives results that drift depending on order. How do math.fsum, pairwise or Kahan summation, and sorting help, and when are they worth it?
  2. 2math.isclose(1e-10, 0.0) returns False. Explain why, and how you would choose abs_tol versus rel_tol for a physical-measurement dataset.

8+ years experience

  1. 1Design coordinate matching or deduplication in a geospatial system where positions are floats. Why does tolerance-based equality break hashing and transitivity, and what would you do instead (e.g. grid snapping)?
  2. 2A distributed job computes the same aggregate on different machines and gets slightly different last digits. Discuss float non-associativity, determinism and strategies for reproducible results.

Follow-up Questions

  • Why does math.isclose(1e-10, 0.0) return False by default, and how do you fix it?
  • What is the difference between Decimal(0.1) and Decimal('0.1')?
Share

Share via WhatsApp, X, Facebook, LinkedIn or copy link. Open Graph preview enabled.