JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Why 0.1 + 0.2 Is Not 0.3

Type 0.1 + 0.2 into almost any language and you get 0.30000000000000004. It is not a bug — it is a precise, predictable consequence of how the machine stores numbers. Here is exactly what happens, and what it teaches you about every numerical computation you will ever run.

The famous result, demystified

By now you know the machine does not store the real number line. It stores a finite set of floating-point values, each a sign, a significand, and an exponent, packed in binary under the IEEE 754 standard. The previous two guides set this up; now we cash it in on the most infamous one-liner in computing. When you type `0.1 + 0.2` and see `0.30000000000000004`, nothing went wrong. Every step did exactly what the standard says — and the tiny leftover is the visible footprint of a decimal you cannot write in binary.

The root cause is that 0.1 is not a finite binary fraction. In base 2, one tenth is 0.0001100110011001100... repeating forever, exactly the way 1/3 is 0.3333... repeating in base 10. A finite-width significand cannot hold an infinite tail, so the machine keeps the leading 53 significant bits and rounds the rest. The stored value is not 0.1; it is the nearest representable number to 0.1, off by a sliver of about 5.55e-18. The same happens to 0.2, and to most innocent-looking decimals.

A clean way to see it: in double precision, 0.1 is stored as 0.1000000000000000055511151231257827021181583404541015625. That is the truth the computer holds — not the decimal you typed. The display routine usually hides the tail and prints '0.1', which is why the surprise only surfaces once several rounding errors happen to line up the way they do in 0.1 + 0.2.

Walking the addition, step by step

Recall the rounded-operation model from the previous guide: the computed result of an operation is the exact result, rounded to the nearest floating-point number under the current rounding mode. Each basic operation carries a relative error no larger than the unit roundoff u, which for doubles is about 1.11e-16. So `0.1 + 0.2` runs the gauntlet of rounding three separate times — once each as the inputs were stored, and once for the sum itself.

  1. Store 0.1: round it to the nearest double. The result is just above 0.1, by about 5.55e-18.
  2. Store 0.2: round it to the nearest double. The result is just below 0.2, by about 1.11e-17.
  3. Add the two stored values exactly. Their true sum lands between two representable numbers and must itself be rounded.
  4. The nearest double to that sum happens to be 0.30000000000000004, not the double nearest to 0.3 — the gaps just do not align. That last mismatch is the whole show.

Here is the subtle twist worth savoring: the double nearest to 0.3 and the double you get from adding the stored 0.1 and 0.2 are different representable numbers, one notch apart. There is no value to blame and no operation that misbehaved. The errors were each below u, but they did not cancel — they nudged the sum just past the midpoint, into the next available slot. That is why the comparison `0.1 + 0.2 == 0.3` returns false in language after language.

Never test floating-point for equality

The first practical lesson is blunt: `==` is the wrong tool for floating-point results. Two computations that are mathematically identical can produce neighboring doubles, so exact equality tests are brittle. Instead, ask whether two values are close, using a tolerance that respects the magnitude of the numbers. Writing `if x == 0.3` is fragile and often false even when x 'should' be 0.3; a relative test such as `abs(x - 0.3) <= tol * max(1.0, abs(x))` with a small `tol` is the robust replacement. A tolerance like a few times the unit roundoff per operation is a sane starting scale.

Why addition is not associative

The 0.1 + 0.2 story is a special case of a deeper truth: floating-point addition is not associative. In exact arithmetic (a + b) + c equals a + (b + c) always; in floating point the two groupings can give different doubles, because each '+' rounds its own intermediate result. Try a = 1e16, b = 1.0, c = -1e16. Then (a + b) + c gives 0.0: adding 1.0 to 1e16 rounds straight back to 1e16, so the small term is swallowed before the big numbers cancel. But a + (b + c) gives 1.0: the giants cancel first, leaving the 1.0 intact.

Two distinct effects are at work. First, absorption: when you add a tiny number to a huge one, the tiny number falls below the gap between adjacent doubles at that magnitude and simply vanishes. Second, catastrophic cancellation: when two nearly equal large numbers subtract, the leading digits agree and disappear, promoting low-order rounding noise into the leading digits of the answer. Order decides which effect strikes first, and so the order of summation changes the result.

This is not a curiosity for contrived inputs. Summing a long list of numbers — a column of a spreadsheet, a million residuals, the terms of a slowly converging series — accumulates one rounding error per addition. In the worst case the total error grows like n times u for n terms, and reordering the list genuinely changes the answer's last digits. Honest takeaway: there is no single 'correct' sum of many floating-point numbers; there is the sum your algorithm computed, and good algorithms keep that close to the exact one.

Summing better: from naive to Kahan

If order matters and naive summation can lose accuracy, can we sum smarter? Yes. The cheapest trick is to sort and add from smallest magnitude up, so tiny terms accumulate among themselves before meeting the giants — this mitigates absorption but does not cure it. The real fix is Kahan summation, also called compensated summation. It carries a second running variable that captures the low-order bits lost in each addition, then feeds that compensation back into the next step. The result behaves as if the additions were done in far higher precision, at the cost of a few extra flops per term.

function kahan_sum(x[1..n]):
    s = 0.0          # running sum
    c = 0.0          # running compensation (lost low bits)
    for i = 1 to n:
        y = x[i] - c         # add back what we lost last time
        t = s + y            # this rounding may drop low bits of y
        c = (t - s) - y      # recover exactly those dropped bits
        s = t
    return s
Kahan summation: the variable c remembers each addition's lost low-order bits.

The single line c = (t - s) - y looks like it must be zero — algebraically t equals s + y, so the whole thing is zero. It is not zero in floating point, and that is precisely the point. Because t = s + y was rounded, (t - s) recovers the part of y that actually made it into the sum, and subtracting y leaves exactly the bits that were dropped. Kahan summation turns the worst-case error from growing like n*u down to roughly a small constant times u, almost independent of how many terms you add — a beautiful example of designing an algorithm around how rounding really works.

What this really teaches you

Step back and the 0.1 + 0.2 puzzle stops being a quirk and becomes a creed for the rest of this ladder. Floating point is not broken real arithmetic; it is its own exact, well-defined arithmetic on a finite set of numbers, with rounding at every step. Every numerical answer you compute is approximate. The interesting question is never 'is it exact?' but 'how large is the error, and can I bound it?' That mindset — quantifying error rather than denying it — is what separates a working scientific computation from a plausible-looking wrong one.

The next guide pushes the cancellation thread further: we will see how the textbook quadratic formula can lose nearly all its digits on perfectly ordinary inputs, and rewrite it into a stable form that does not. And when you eventually want a high-accuracy total over a huge dataset, you will reach for compensated summation without a second thought — because you now understand, not just that 0.1 + 0.2 misbehaves, but exactly why, and what to do about it.