Open a Python REPL, a Node console, or a Ruby prompt and type 0.1 + 0.2. You will not get 0.3. You will get 0.30000000000000004. This is not a bug in any one language — it is how nearly all hardware represents fractional numbers, and understanding it will save you from a class of subtle errors.
Numbers Are Stored in Binary, and Binary Has Limits
Most languages represent decimals using IEEE 754, the standard for binary floating-point arithmetic. A typical double (64-bit float) packs a number into three parts: a sign bit, an exponent, and a mantissa (also called the significand). Think of it as scientific notation in base 2: the value is roughly sign × mantissa × 2^exponent. With 64 bits, the mantissa gets 52 bits, giving you about 15 to 17 significant decimal digits of precision.
The catch is the base 2 part. Just as you cannot write 1/3 exactly in decimal — it is 0.3333... forever — you cannot write certain decimal fractions exactly in binary. A fraction has a finite binary representation only if its denominator is a power of two. So 0.5 (1/2) and 0.25 (1/4) are exact. But 0.1 is 1/10, and 10 is not a power of two, so in binary 0.1 becomes a repeating fraction:
0.1 (decimal) = 0.0001100110011001100... (binary, repeating forever)
Since the mantissa is finite, the machine stores the nearest representable value to 0.1, not 0.1 itself. The same happens to 0.2. Neither is the number you typed — each is off by a tiny amount.
Where the Stray Digits Come From
When you compute 0.1 + 0.2, you are not adding the true values. You are adding the two rounded approximations, then rounding the result again to fit back into a double. Those small errors happen to push the sum just above the stored value of 0.3.
>>> 0.1 + 0.2
0.30000000000000004
>>> (0.1 + 0.2) == 0.3
False
>>> 0.1 + 0.2 - 0.3
5.551115123125783e-17
The leftover 5.5e-17 is the accumulated rounding error. With a single operation it is invisibly small, but errors can accumulate: a loop that adds 0.1 ten thousand times will drift measurably away from 1000. The more operations you chain, the more rounding compounds.
Because two mathematically equal calculations can produce slightly different bit patterns, direct equality on floats is unreliable. Instead, check whether the difference is smaller than a small tolerance — an epsilon. For example,
abs(a - b) < 1e-9. Many languages ship a helper, such as Python'smath.isclose(), that picks a sensible relative-and-absolute tolerance for you.
When Floats Are the Wrong Tool
Floating point is the right choice for measurements, physics, graphics, and machine learning — anywhere small relative error is acceptable and speed matters. It is the wrong choice when exactness is required, and money is the classic example. Charging a customer $0.30000000000000004 or watching a running total drift by a penny is unacceptable.
You have two solid alternatives:
-
Integers in the smallest unit. Store money as a whole number of cents (or satoshis, or tenths of a cent) and only format to a decimal point for display.
30cents is exact;0.30dollars is not. -
A decimal or fixed-point type. Many languages provide arbitrary-precision decimal types — Python's
decimal.Decimal, Java'sBigDecimal, C#'sdecimal, SQL'sNUMERIC/DECIMAL. These store numbers in base 10, so 0.1 + 0.2 is exactly 0.3, at the cost of speed.
The rule of thumb: if a human will count the result and care about the last digit, do not use a raw float.
Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.
Top comments (0)