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

Floating Point: Why 0.1 Isn't Exact

Type 0.1 + 0.2 into almost any language and you get 0.30000000000000004. That stray tail is not a bug — it is honest arithmetic in base two. Let's see exactly where it comes from and how a float really stores a number.

The number that won't sit still

By now you can read a binary or hex number, and you have seen how the same bit pattern means different things depending on how you read it — an unsigned integer, a two's-complement signed integer, and so on. Integers are tidy: every value in range has exactly one pattern, and the arithmetic is exact. So it comes as a shock the first time a computer tells you, with a perfectly straight face, that 0.1 + 0.2 is 0.30000000000000004. Nothing is broken. The machine is being completely honest — it is just not using the number system you expected.

The reason is the same reason 1/3 looks ugly in our everyday base ten. We write 1/3 as 0.3333... and the threes never stop; ten simply has no finite decimal for one third. A computer stores fractions in base two, and in base two it is 0.1 that has no finite ending. Write it out and you get 0.0001100110011001100... repeating forever. The machine has only a fixed number of bits, so it must chop that endless tail off somewhere — and the value it actually keeps is a hair away from the 0.1 you typed.

Scientific notation, in bits

To store numbers that span from the size of an atom to the size of a galaxy, computers borrow the idea of scientific notation. In base ten you write a big or tiny number as a single significant part times a power of ten: 6.022 x 10^23, or 1.6 x 10^-19. A floating-point number does exactly this in base two. Every value is split into a sign (plus or minus), a mantissa — the significant digits, also called the significand — and an exponent that says how far to slide the binary point left or right. That sliding point is literally what 'floating' means.

The exact layout is fixed by a standard everyone agrees on, IEEE 754, so a float written on one machine reads the same on another. The 32-bit form (C's `float`) uses 1 bit for the sign, 8 bits for the exponent, and 23 bits for the mantissa. The 64-bit form (C's `double`, and what most languages call a number by default) uses 1 + 11 + 52. More mantissa bits mean more significant digits of precision; more exponent bits mean a wider range of magnitudes. Just like the integer encodings you already met, a float is only a bit pattern plus an agreed rule for reading it.

double (IEEE 754, 64 bits):

  S |     exponent (11)      |              mantissa (52)               |
  +-+-----------------------+------------------------------------------+
  |1|        biased         |   fraction after the implied leading 1   |
  +-+-----------------------+------------------------------------------+

  value = (-1)^S  x  1.mantissa  x  2^(exponent - 1023)
A double laid out left to right: one sign bit, eleven exponent bits, fifty-two mantissa bits. The leading 1 before the binary point is not stored — it is implied.

Two small but clever details make this efficient. First, the leading bit of the mantissa is always 1 in normalized scientific form (just as a decimal mantissa always starts 1-9, never 0), so IEEE 754 simply does not store it — you get a free 53rd bit of precision out of 52 stored bits. Second, the exponent is stored as a biased value: the raw 11-bit field holds the true exponent plus 1023, so the field is always a plain unsigned number and comparing two positive floats works almost like comparing integers. The sign bit sits at the very top, exactly where the sign bit lives in your signed integers.

Tracing where 0.1 goes wrong

Let's pin down the famous example by hand, the same careful way you did base conversion for integers — except now we convert the fraction. The trick for a fractional part is to multiply by two over and over: each time, the integer part you cross (0 or 1) is the next bit after the binary point.

  1. Start with 0.1. Multiply by 2: 0.2, integer part 0. First bit after the point: 0.
  2. 0.2 x 2 = 0.4, integer part 0. Next bit: 0. Then 0.4 x 2 = 0.8, bit 0. Then 0.8 x 2 = 1.6, bit 1, and we carry the 0.6 onward.
  3. 0.6 x 2 = 1.2, bit 1; keep 0.2. But we have seen 0.2 before! From here the sequence 0011 repeats forever: 0.1 = 0.0001100110011001100... in binary.
  4. A double has only 52 mantissa bits, so it must round this endless tail to the nearest value it can hold. The stored number is very slightly larger than 0.1 — roughly 0.1000000000000000055511151231257827.

Now the punchline. The 0.2 you type is also rounded, and so is 0.3. When you add the slightly-too-big 0.1 to the slightly-too-big 0.2, the two tiny errors combine into a sum that, rounded to the nearest double, lands just past the rounded 0.3 — and printing it in full reveals 0.30000000000000004. Every step obeyed round-to-nearest perfectly. The result is exactly what honest base-two arithmetic must produce; there was never a moment of misbehavior to fix.

Infinity, NaN, and the edges of the line

IEEE 754 does something integers never do: it reserves a few exponent patterns for special values that are not ordinary numbers. When the exponent field is all ones, the value is no longer a finite number. If the mantissa is zero you get infinity — positive or negative, depending on the sign bit — which is what 1.0 / 0.0 produces instead of crashing. If the mantissa is non-zero you get NaN, 'not a number', the result of nonsense like 0.0 / 0.0 or the square root of a negative.

NaN has a property that catches everyone once: it is not equal to anything, not even to itself. The test `x == x` is true for every normal number but false when x is NaN, which is the standard trick for detecting one. So infinity and NaN are not error codes smuggled into the number — they are genuine, well-defined values that arithmetic flows through, letting a long calculation finish and report 'this part blew up' at the end rather than halting in the middle.

Living with floats honestly

The first practical rule falls straight out of all this: never test two floating-point results for exact equality. Asking `if (a == b)` after any arithmetic is fragile, because each operation may nudge the result by a tiny rounding step. Instead, check whether they are close enough — that the absolute difference is smaller than some small tolerance you choose for the problem at hand. 'Equal' is the wrong question for floats; 'near enough' is the right one.

A second honest caution: the precision is not the same everywhere on the number line. A double carries about 15 to 16 significant decimal digits, but those digits are relative — the gap between representable values is tiny near zero and grows enormous near the top of the range. Add a small number to a huge one and the small one can vanish entirely, swallowed below the last bit. This is also why money is usually not stored in floats: cents must be exact, so financial code keeps integers (counting whole cents) or a dedicated decimal type instead.

None of this makes floating point bad — it is a brilliant, standardized way to pack an astonishing range of real numbers into a fixed handful of bytes, and it powers physics engines, graphics, and machine learning every second. The point is to respect what it is. It trades a little exactness for an enormous range, it rounds at every step, and it has well-defined infinities and NaNs at its edges. Understand those tradeoffs and 0.30000000000000004 stops being a surprise and becomes a signature — proof the machine did precisely, faithfully, what base two demands.