Skip to content
Published on

Floating Point: Why 0.1 + 0.2 ≠ 0.3

Authors

Introduction — The One Line That Confuses Everyone

Anyone who has done a bit of programming eventually runs into this scene. You open a console, ask for the simplest possible addition, and the result is strange.

>>> 0.1 + 0.2
0.30000000000000004

The first time you see it, you wonder if the computer is broken. Even a schoolchild knows that 0.1 plus 0.2 is 0.3, so why does this expensive machine produce something bizarre like 0.30000000000000004? And it is not just Python. JavaScript, Java, C, Go, Ruby, nearly every mainstream language gives the same answer.

Let me say up front: this is not a bug. It is not a flaw in the language, nor an error in the CPU. It is the inevitable result of how computers store real numbers, IEEE 754 floating point. And its root boils down to one surprisingly simple fact. Computers store numbers in binary, and 0.1 cannot be written exactly in binary. This post digs into why, from the ground up.

Computers Live in Binary

Just as we use base 10, computers use base 2. In base 10, the fraction 0.75 means "seven tenths plus five hundredths." In base 2, a fraction is expressed as a sum of "one half, one quarter, one eighth, and so on."

  base 10:  0.75 = 7/10 + 5/100

  base 2:   0.11 = 1/2 + 1/4 = 0.5 + 0.25 = 0.75  (exact)

Some numbers land exactly in binary. 0.5 is one half, so it is 0.1 in binary; 0.25 is one quarter, so 0.01; 0.75 is 0.11, exactly. Any fraction whose denominator is a power of two has a finite representation in binary.

The trouble is numbers that are not like that. 0.1 is one tenth, and 10 is not a power of two. Try to write this number in binary and you get a non-terminating, infinitely repeating fraction.

  0.1 (base 10) in binary:
  0.0001100110011001100110011001100110011...  (0011 repeats forever)

This is exactly the same phenomenon as trying to write one third in base 10, where 0.333333... never ends. Just as one third has no finite decimal, one tenth has no finite binary. Only the base differs; the principle is identical.

A computer cannot store infinitely many digits. So it must cut off somewhere. The moment you store 0.1, the computer stores not the true 0.1 but "the closest binary number, representable in a finite number of digits, to 0.1." That tiny error is where the whole story begins.

IEEE 754 — The Standard Container for Real Numbers

So how are those finite digits arranged? Here is where the IEEE 754 standard comes in. Almost all hardware today stores real numbers this way. The core idea is scientific notation.

Just as we write a very large or small number as 6.022 × 10^23, floating point splits a number into three parts: the sign, the mantissa (also called the significand), and the exponent.

  value = (-1)^sign x mantissa x 2^exponent

  sign      : positive or negative (1 bit)
  exponent  : where to put the point (shifts the digits)
  mantissa  : the significant digits (carries the precision)

The most widely used 64-bit double precision (double) divides its 64 bits like this.

  64-bit double:
  [ sign 1 bit ][ exponent 11 bits ][ mantissa 52 bits ]

  32-bit float:
  [ sign 1 bit ][ exponent 8 bits ][ mantissa 23 bits ]

Here the meaning of the name "floating" appears. The position of the point is not fixed; it floats, moving according to the exponent. Increase the exponent to represent very large numbers, decrease it for very small ones, all with the same number of bits. Thanks to this flexibility, floating point covers a wide range, from the size of an atom to the size of a galaxy.

But there is a core constraint: the number of mantissa bits is finite. A double has only 52 mantissa bits, so it can hold roughly 15 to 17 significant decimal digits. Any precision beyond that is discarded. So a binary fraction that goes on forever, like 0.1, is cut off at 52 bits, and that truncated value is what gets stored.

So Why Is 0.1 + 0.2 Not 0.3?

Now we can solve the opening mystery. When you store 0.1 in a computer, what is actually stored is a value slightly larger than the true 0.1. Likewise, 0.2 is stored as a slightly different value.

  value you want    value actually stored (approximate)
  0.1        ->    0.1000000000000000055511151231257827021181583404541015625
  0.2        ->    0.2000000000000000111022302462515654042363166809082031250

Add these two approximations and the errors add up too. Their sum is also slightly off from the approximation of the true 0.3.

  0.1(approx) + 0.2(approx) = 0.3000000000000000444089...
  approximation of 0.3       = 0.2999999999999999888977...

  the two differ! so 0.1 + 0.2 == 0.3 is false

So three separate rounding errors (the error in 0.1, the error in 0.2, and the error when storing their sum again) stack up to produce the visible 0.30000000000000004. The computer computed perfectly accurately. It is just that the ingredients it stored in the first place were not the true 0.1 and 0.2.

One reassuring fact is that this error is not random but deterministic. The same operation always produces the same error. So it is reproducible, predictable, and manageable. The problem is not the existence of error itself, but writing code without knowing about it.

Do Not Compare Reals Exactly — Epsilon

The most common mistake when working with floating point is comparing two reals directly with ==. As we saw, 0.1 + 0.2 is not exactly equal to 0.3, so this kind of code behaves contrary to expectations.

if 0.1 + 0.2 == 0.3:
    print("equal")
else:
    print("not equal")   # this branch actually runs

The correct approach is to ask not "are they exactly equal?" but "are they close enough?" If the difference between two values is smaller than a tiny tolerance (epsilon), we treat them as equal.

def close_enough(a, b, epsilon=1e-9):
    return abs(a - b) < epsilon

print(close_enough(0.1 + 0.2, 0.3))   # True

But there is a trap here too. A fixed epsilon (say 1e-9) may not be appropriate depending on the magnitude of the values. When comparing very large numbers, a difference of that size may be smaller than the natural error and cause a false failure; when comparing very small numbers, it may be far too lenient. So in practice we use an approach that considers both absolute and relative error.

  absolute-error comparison:  |a - b| < eps
    good for small numbers, poor for large numbers

  relative-error comparison:  |a - b| <= eps * max(|a|, |b|)
    scales the tolerance with the magnitude of the values

  practical libraries combine the two
  (e.g. Python's math.isclose looks at both relative and absolute)

Standard functions like Python's math.isclose and NumPy's numpy.allclose already implement this combined approach. If choosing an epsilon yourself is hard, using such a battle-tested function is the safe move.

Never Handle Money with float

The place where floating-point error shows up most dangerously is financial calculation. Money must be exact. An error of one cent is unacceptable in accounting, and if such errors accumulate over millions of transactions, they become real losses. Yet handle money with float and that very error seeps in.

# bad: money math with float
price = 0.1
total = 0.0
for _ in range(10):
    total += price
print(total)          # 0.9999999999999999  — not 1.0!

Adding 0.1 ten times should give 1.0, but for the reason we saw it comes out slightly off. Building an invoice or comparing a balance with such a value is a disaster. There are two directions for a fix.

1. Use integers (smallest unit). Store amounts not as dollars but as integers of the smallest currency unit (cents, for example). For 1,234.56 dollars, store 123456 cents. Integer arithmetic has no error at all, so it is perfectly exact. You only insert the decimal point when displaying to the screen.

# good: handle as integers (cents)
price_cents = 10          # 0.10 dollars as 10 cents
total_cents = 0
for _ in range(10):
    total_cents += price_cents
print(total_cents / 100)  # 1.0  — exact!

2. Use a decimal type. Most languages provide a decimal type that handles base 10 as-is. Python's decimal.Decimal and Java's BigDecimal are the classic examples. They store decimal digits internally, so they treat 0.1 as the true 0.1.

from decimal import Decimal

a = Decimal("0.1")
b = Decimal("0.2")
print(a + b)              # 0.3  — exact!
print(a + b == Decimal("0.3"))   # True

There is an important detail here. When creating a Decimal, you must pass a string (Decimal("0.1")). If you pass a float like Decimal(0.1), the already-error-laden float value goes straight in, and the benefit of decimal vanishes. If the ingredient is contaminated, no matter how precise the container, it is useless.

In summary: integer handling suits high-volume calculations where performance matters and the unit is clear; decimal handling suits accounting logic where readability and arbitrary precision matter. Either way, float alone must be avoided.

NaN, Infinity, and Negative Zero

IEEE 754 defines a few special values beyond ordinary numbers. Not knowing them leads to surprising bugs.

Infinity. Exceed the largest representable number (overflow), or divide a nonzero number by zero, and you get infinity. There is a separate positive infinity and negative infinity.

print(1e308 * 10)   # inf   (overflow)
print(-1e308 * 10)  # -inf

NaN (Not a Number). The result of an undefined operation. Divide zero by zero, subtract infinity from infinity, or take the square root of a negative number, and you get NaN. NaN's most infamous property is that it is not equal even to itself.

nan = float("nan")
print(nan == nan)   # False!  — NaN equals nothing, not even itself
print(nan != nan)   # True

# so you check for NaN with a dedicated function, not ==
import math
print(math.isnan(nan))   # True

This property seems strange at first, but the standard defines it that way. As a result, an idiom for "is this value NaN?" is x != x. If it differs from itself, it is NaN. Still, using an explicit isnan function reads better.

Negative zero (-0.0). Floating point has a separate positive zero and negative zero. The two compare as equal with ==, but behave differently in subtle situations. For example, when dividing by zero, the sign decides between positive and negative infinity.

print(0.0 == -0.0)      # True   (equal by comparison)
print(1.0 / 0.0)        # to reach inf you need separate handling (Python raises)
# in C, Java, etc.:  1.0/0.0 -> +inf,  1.0/-0.0 -> -inf

Negative zero is usually nothing to worry about, but in numeric computation where sign carries meaning (limits, complex numbers, certain physics simulations), it can create subtle differences.

Accumulation Error — When Small Errors Pile Up

The errors we have seen are each tiny (around the sixteenth decimal place). But repeat an operation millions of times and these small errors can accumulate and grow to a visible magnitude. It is especially severe when adding numbers of very different sizes.

# keep adding a small number to a big one and the small one gets "swallowed"
big = 1e16
small = 1.0
print(big + small)        # 1e16  — small disappeared!
print(big + small == big) # True

What happened here? A double's mantissa holds only about 16 significant digits. 1e16 is already at the edge of that precision, so adding 1.0 pushes that 1 below the representable digits, where it is rounded away and vanishes. This is called absorption: the big number swallows the small one.

This phenomenon becomes a real problem when summing many numbers. Naively adding left to right, the larger the running sum grows, the more the small values added later get ignored. A famous technique to mitigate this is Kahan summation. It separately remembers the error discarded at each step and compensates for it in the next step.

def kahan_sum(numbers):
    total = 0.0
    compensation = 0.0   # remembers the lost low-order bits
    for x in numbers:
        y = x - compensation
        t = total + y
        compensation = (t - total) - y   # the error lost this round
        total = t
    return total

Through error compensation, Kahan summation produces a far more accurate result than the naive sum. In fields that handle massive amounts of real arithmetic, such as data analysis, scientific computing, and graphics, these numerical stability techniques matter. The key lesson is that errors, small individually, grow within repetition, and that the order and method of operations affect accuracy.

A Summary of Practical Rules

Here are the practical rules for handling floating point safely, in compressed form.

  • Do not compare reals with ==. Use epsilon-based approximate comparison (math.isclose, etc.) instead.
  • Do not handle money with float. Use integers (smallest unit) or a decimal type.
  • Create decimals from strings. Decimal("0.1"), not Decimal(0.1).
  • Check for NaN with a dedicated function. x == nan is always false, so use isnan.
  • Watch out for sums of very differently sized numbers. If needed, use a stable technique like Kahan summation.
  • Separate display rounding from computational precision. Show two decimals on screen, but keep internal calculations at higher precision.
  • If precision matters extremely, use an arbitrary-precision library. But you sacrifice speed.

The shared spirit of these rules is to always be conscious of the fact that floating point is an approximation. Handled with that awareness, it is a powerful tool; mistaken for exact values, it is a trap that silently produces wrong results.

Conclusion

0.1 + 0.2 not being 0.3 is not a mistake by the computer but the result of a fundamental compromise: representing infinitely many reals with finitely many bits. Computers store numbers in binary, and since many decimal fractions like one tenth go on forever in binary, they must be rounded somewhere. IEEE 754 packs this compromise elegantly into sign, exponent, and mantissa, trading wide range for practical precision.

Once you understand this, floating point is no longer capricious magic but a predictable tool for those who know the rules. Need exact comparison? Use an epsilon. Need exact money? Use integers or decimal. Need stable large-scale summation? Use a numerical stability technique. The core is one thing: floating point is an approximation that imitates real numbers, not real numbers themselves. Remember that single sentence and you can avoid most floating-point traps.

References