Why 0.1 + 0.2 Isn't 0.3
In short: a float is 64 stored bits — sign, exponent and mantissa — and most
decimal fractions have no exact binary form, the same way 1/3 has no exact decimal
form. Python stores the double closest to the number you typed, so 0.1 is already a
tiny bit off before any arithmetic happens, and each later + can round again. Never
compare computed floats with ==; use math.isclose (with abs_tol when either side
might be 0), and never store money in a float.

A Quarry user runs a report that sums two price columns and checks the answer
against a stored total. The check fails for a row where the sum is 0.1 + 0.2 and the
total is 0.3. There's nothing wrong with Python. The numbers were never 0.1, 0.2 and
0.3 in the first place. A float is 64 bits laid out in fields, just like Quarry's
record header, and most decimal fractions don't fit in them exactly.
This lesson opens those 64 bits with the same shift-and-mask tools from lesson 2. You'll see what's really stored, why sums drift, and what a storage engine should do about it.
Why 0.1 has no exact binary form
In decimal, 1/3 never ends: 0.333… A fraction ends in base 10 only if its denominator is made of 2s and 5s. In base 2 the rule is stricter: the denominator must be a power of two. 1/10 has a factor of 5, so in binary it repeats forever.
You can generate the binary digits the way you'd do long division. Double the remainder. If it reaches the denominator, the next bit is 1 and you subtract. Trace the first twelve bits of 1/10:
0.0001100110011…, where the block 0011 repeats forever. A float has room for 53
significant bits, so it keeps the first 53 and rounds. The stored 0.1 is the double
closest to 0.1, which is slightly above it.
Try the doubling loop yourself. It draws each bit as it's produced. Change the fraction
to 1, 4 (a power-of-two denominator), 1, 3 or 3, 5, and see which ones finish
and which settle into a repeating pattern:
Your long division in base 2. A denominator that isn't a power of two never reaches remainder 0, so the bits go on forever.
The 64 bits of a double
IEEE 754 double precision splits a word into three fields, the same idea as Quarry's header:
| Field | Bits | Width | Meaning |
|---|---|---|---|
| sign | 63 | 1 | 0 positive, 1 negative |
| exponent | 62–52 | 11 | stored with a bias of 1023, so 1023 means 2⁰ |
| mantissa | 51–0 | 52 | the fraction after an implied leading 1. |
A normal double is (-1)^sign × 1.mantissa × 2^(exponent − 1023). The implied leading
1 is free, so 52 stored bits give 53 bits of precision. Step through 0.1, whose 64 bits
are drawn here as four rows of 16:
0.1 as a double. The pattern that should repeat forever is cut off after 52 bits and rounded, and that last rounding is where the error comes from.
The sign bit is 0, so the number is positive. 4 by 16 table. 64 of 64 cells filled.
You can read the fields out of any float with struct. It turns the double into 8
bytes, the bytes into an int, and then it's shift-and-mask as usual:
The last two lines are the whole story. 0.3 and 0.1 + 0.2 have the same sign and
exponent, and their mantissas differ by exactly one in the last place. Each input
was rounded up a little. The exact sum of the two rounded values lies precisely halfway
between two doubles, and round-to-nearest-even sends it to the upper one. That's 0.3
plus one unit in the last place (ULP).
print(0.1 + 0.2 == 0.3, round(0.1 + 0.2, 10) == 0.3)Gaps between doubles grow with size
53 bits of precision means the gap between neighbouring doubles is relative. Near 1
it's 2⁻⁵², about 2.2e-16. Near a million it's about 1.2e-10. And past 2⁵³ the
gap is 2, so doubles can't even represent every integer any more:
print(2.0 ** 53 + 1 == 2.0 ** 53)Quarry stores timestamps. If one is a float counting seconds since 1970, how long does
it keep microsecond precision? The gap at t seconds is 2^(floor(log₂ t) − 52),
which is exactly what math.ulp measures:
The gap passes a microsecond in year 273, so 2243, when the timestamp passes 2³³ seconds. It doubles at every power of two, so it grows in steps rather than smoothly. For Quarry the lesson is to keep time as an integer count of microseconds. A 64-bit integer lasts 290,000 years and never loses a tick.
Errors accumulate
One rounding error is tiny. A long loop of additions piles them up, because each +=
rounds again:
total = 0.0
for _ in range(10):
total += 0.1
print(total == 1.0)Three rules keep Quarry correct:
- Never compare computed floats with
==. Usemath.isclose(a, b), which compares relative to the size of the numbers. Addabs_tolwhen either might be 0. - Don't store money in floats. Store integer cents, or use
decimal.Decimal, which does exact decimal arithmetic. Quarry's price column is an integer. - Sum carefully.
math.fsumreturns the correctly rounded sum of the exact inputs, andsum()compensates in 3.12+. A plain loop does neither.
This reconciliation check fails on data that's actually fine:
It prints False True. The first check compares two doubles that differ by one ULP. Make the comparison tolerant of rounding.
Interview corner — is this string a price you can store?
Given prices as strings like "19.99", "5" or "0.3", total them exactly and
return the total in cents. Converting each one to float and multiplying by 100 invites
errors like 0.29 * 100 = 28.999999999999996. Parse the digits as integers instead:
Quick practice
print(0.1 + 0.1 == 0.2)It prints True False. 0.3 - 0.1 - 0.2 isn't exactly 0 (off by about -2.78e-17), and isclose's default tolerance is relative, so near zero it's an almost-zero tolerance too.
🎯 Your turn
Write float_fields(x) returning [sign, exponent, mantissa], the three raw bit
fields of the double x. The exponent is the 11-bit field as stored (biased), and the
mantissa is the 52-bit field.
float_fields(0.1)→[0, 1019, 2702159776422298]float_fields(-2.0)→[1, 1024, 0]
Hint — int.from_bytes(struct.pack(">d", x), "big") gives the 64 bits as an
integer. Then shift and mask: the sign is >> 63, the exponent is >> 52 masked with
0x7FF, and the mantissa is masked with (1 << 52) - 1.
Then press ▶ Run, try the Live App chips, and hit ✓ Check. ✅
