Run 0.1 + 0.2 in a Python shell and it does not return 0.3. It returns 0.30000000000000004. Try it in JavaScript, Java, Go, C, or Rust and you get the same sixteen-digit tail. Every one of those languages is doing exactly what the hardware standard tells it to. A number that needs infinitely many binary digits is being squeezed into 64 bits, and something has to give.

Why 0.1 can’t be stored exactly in binary

Computers store floating-point numbers in base 2. In base 2, the fraction 1/10 is a repeating expansion, the same way 1/3 repeats forever in base 10 (0.3333...). Written out, 0.1 in binary starts:

1
0.1₁₀ = 0.000110011001100110011...₂  (repeating forever)

No finite number of bits cuts that off and gives you 0.1 back. A 64-bit IEEE 754 “double” (1 sign bit, 11 exponent bits, 52 stored mantissa bits, 53 bits of precision once you count the implicit leading 1) rounds the infinite expansion to the nearest value it can represent, which is always a fraction of the form J / 2^N. For 0.1, that value is:

1
3602879701896397 / 2^55

As a decimal, that is 0.1000000000000000055511151231257827021181583404541015625. Python will show it to you. Every snippet in this article uses only the standard library, so you don’t need to set up a virtual environment to follow along:

1
2
3
4
>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)
>>> 0.1 == 0.1000000000000000055511151231257827021181583404541015625
True

0.2 carries its own, slightly different rounding error. Add the two already-imperfect values and the errors don’t cancel. They combine into a result one step above the double closest to 0.3. Hence 0.30000000000000004.

Which numbers floating point actually stores exactly

Not every decimal is doomed. A binary fraction terminates when its denominator, in lowest terms, is a power of two. 0.5 is 1/2. 0.25 is 1/4. 0.125 is 1/8. Those are exact:

1
2
3
4
>>> 0.5 + 0.25 == 0.75
True
>>> 0.1 + 0.2 == 0.3
False

The decimals people actually type, like prices and percentages, almost never have a power-of-two denominator. That makes the 0.1 + 0.2 error the normal outcome of decimal arithmetic on binary floats, and it accumulates. Add 0.1 ten times in any language that uses doubles and you don’t land on 1.0:

1
2
3
4
5
>>> total = 0.0
>>> for _ in range(10):
...     total += 0.1
>>> total
0.9999999999999999

Ten additions. That’s all it takes. Note that Big O analysis tells you how fast that loop grows with input size and nothing about whether its answer is right; correctness here is a separate check you have to make yourself.

How to compare floating-point numbers safely

Since == on floats can’t be trusted, compare with a tolerance: check that two numbers are close enough instead of identical. Every mainstream language has an idiom for it.

LanguageSafe comparison
Pythonmath.isclose(a, b) — relative tolerance 1e-9 by default
JavaScriptMath.abs(a - b) < Number.EPSILON * scale
JavaMath.abs(a - b) < 1e-9, or BigDecimal for exact decimal
C#Math.Abs(a - b) < 1e-9, or the native decimal type
Gomath.Abs(a-b) <= tolerance (no built-in helper)

In Python:

1
2
3
4
5
>>> import math
>>> 0.1 + 0.1 + 0.1 == 0.3
False
>>> math.isclose(0.1 + 0.1 + 0.1, 0.3)
True

math.isclose scales its tolerance to the size of the numbers (rel_tol * max(abs(a), abs(b))), so the same call works at small and large magnitudes. Its default abs_tol is 0.0, though. If either value can legitimately be zero or very close to it, pass an explicit abs_tol, or the comparison will fail on values you’d consider equal.

JavaScript’s Number.EPSILON (2⁻⁔ÂČ, about 2.22e-16) is the gap between 1 and the next representable double, so on its own it is only a valid threshold for numbers near 1. Scale it for anything bigger:

1
2
3
4
5
function nearlyEqual(x, y, scale = 1) {
  return Math.abs(x - y) < Number.EPSILON * scale;
}
nearlyEqual(0.1 + 0.2, 0.3);              // true
nearlyEqual(1000.1 + 1000.2, 2000.3, 2000); // true, scale matches the magnitude

An assertion like result == 0.3 in a test is a bug waiting for a different input, platform, or compiler to expose it. Whether it’s a unit test or sits higher up the testing pyramid, replace the exact comparison with a tolerance check.

When floating point is the wrong tool: money and counts

A tolerance fixes comparison. It doesn’t fix storage. If you’re adding up prices or keeping a running balance, the rounding error itself is the problem, and the fix is to stop using float/double for that value.

The simplest version: store money as an integer count of the smallest unit. $19.99 is the integer 1999 cents, not the float 19.99. Integer addition and subtraction have no rounding error. Convert to a decimal string only when you display the value.

When you need real decimal fractions with exact rounding rules (a ledger, a tax calculation, anything that must reconcile to the cent), use a decimal type:

1
2
3
>>> from decimal import Decimal
>>> Decimal("0.1") + Decimal("0.2") == Decimal("0.3")
True

Java has BigDecimal for the same job; C# has a native 128-bit decimal type with about 28-29 significant decimal digits. All three store digits in base 10, so the decimal fractions people write are stored without rounding. You pay for that in speed and verbosity. Decimal types are slower than native floats and need an explicit rounding mode for division. Worth it for a bank balance. A bad trade for a tight numeric loop.

When floating-point imprecision doesn’t matter

Graphics transforms, physics simulations, machine learning weights, and audio processing run on floats on purpose. A relative error around 1e-16 is far below anything those domains can notice, and native float hardware is dramatically faster than decimal arithmetic.

So the question to ask about a number is not whether it has a decimal point. Ask whether you’re counting discrete, exact things, such as money or a test’s expected output, or measuring a continuous quantity that was approximate to begin with, such as a pixel position or a sensor reading. Counted values need integers or a decimal type. Measured values are what floating point was built for, and moving them to Decimal makes them slower without making them meaningfully more correct.

Next time you see a balance field declared float or a test asserting result == 0.3, decide which kind of number it is and fix the storage type or the comparison in that review, before a missing cent shows up in production.