Floating Point
A floating-point number is scientific notation in binary with a fixed budget of bits, and the budget means most decimal fractions cannot be stored exactly. Adding 0.1 and 0.2 gives 0.30000000000000004 in Python, JavaScript, Java and every other language that uses the IEEE 754 standard. None of them has a bug: this is what it costs to cover magnitudes from about 10 to the minus 308 to 10 to the 308 in 64 bits.
This topic draws the picture that makes the price predictable, and follows it to where it lands in real systems: comparisons, sums, sorting, identifiers and money.
Scientific Notation in Binary
A 64-bit float, which is Python's float and JavaScript's ordinary number type, spends 1 bit on the sign, 11 bits on an exponent and 52 bits on a fraction. The value is the fraction, with an implied leading 1, multiplied by 2 raised to the exponent. That gives 15 to 17 significant decimal digits over an enormous range: the largest double is about 1.8 times 10 to the 308, and the smallest ordinary positive one is about 2.2 times 10 to the minus 308. A 32-bit float spends 1, 8 and 23 bits and keeps about 7 significant digits.
The figure decodes two values. The number 1.5 is exact: sign 0, an exponent meaning 2 to the power 0, and a fraction of one half, because 1.5 is 1 plus a half. The number −0.1 is not. Its sign bit is 1 and its exponent means 2 to the minus 4, but its fraction is the repeating pattern 1001 1001 1001, cut off after 52 bits and rounded. What is stored is very close to −0.1 and is not −0.1.
The Uneven Number Line
Because the fraction has a fixed number of bits, every doubling of magnitude holds the same count of values. Between 1 and 2 there are 2 to the 52 evenly spaced doubles. Between 2 and 4 there are the same number, spread over twice the distance, so the gap between neighbours doubles. Floats are dense near zero and sparse far from it.
Follow the doublings far enough and the gap reaches 1. Between 2 to the 52 and 2 to the 53 the spacing is exactly 1, and above 2 to the 53, which is 9,007,199,254,740,992, it is 2. Odd integers above that value cannot be stored at all, and 2 to the 53 plus 1 rounds back to 2 to the 53. A 32-bit float reaches the same wall much sooner: a float32 counter stops increasing at 16,777,216, because adding 1 rounds straight back to the same value.
Why 0.1 + 0.2 Is Not 0.3
One tenth in binary repeats forever, the way one third does in decimal. The stored value is the nearest double, which Python's decimal module prints exactly as 0.1000000000000000055511151231257827, and so on for another 21 digits. The stored 0.2 is slightly high as well, while the double nearest to 0.3 is slightly low.
Add the two slightly-high values and the exact sum lands between two doubles. It rounds to the one immediately above the double nearest 0.3, a single step away, and that neighbour prints as 0.30000000000000004. Equality compares stored values, sees two different doubles, and returns false. Every language that uses IEEE 754 doubles does the same arithmetic and gets the same answer.
Where Precision Goes
Adding a small number to a large one can lose the small one entirely: 10 to the 16 plus 1 is 10 to the 16, because the gap between doubles there is 2. Subtracting two nearly equal numbers cancels their shared leading digits and leaves mostly rounding error behind. And addition is not associative. On Python 3.15, 0.1 plus 0.2, then plus 0.3, gives 0.6000000000000001, while 0.1 plus the sum of 0.2 and 0.3 gives 0.6. Summing the same list in a different order, which is what parallel code and GPUs do, gives a slightly different total.
Repeated addition lets the errors pile up. A loop that adds 0.1 a hundred times ends at 9.99999999999998, and a loop that adds one cent a million times ends at 10,000.000000171856. Python's built-in sum function has used compensated summation since version 3.12 and returns exactly 10,000 for the second list, which hides the drift only until the arithmetic moves into a plain loop. Rounding surprises too: 2.675 is stored as slightly less than itself, so rounding it to two places gives 2.67.
Infinity, NaN and Negative Zero
The standard reserves bit patterns for positive and negative infinity, for "not a number", written NaN, and for a negative zero that compares equal to zero. NaN is what zero divided by zero or infinity minus infinity produces in hardware. Python raises ZeroDivisionError for float division by zero instead of returning infinity, but NaN still arrives through arithmetic, parsing and other libraries.
NaN is not equal to anything, including itself, and every comparison with it is false. That breaks every algorithm built on comparisons. On Python 3.15, sorting the list 3, NaN, 1, 2 returns it unchanged, still unsorted. The max of NaN, 1 and 2 is NaN, while the max of 1, 2 and NaN is 2: the answer depends on where the NaN sat. And one NaN in a sum makes the whole sum NaN.
Floats in Money, Ids and Tests
Money stored as a float drifts a fraction of a cent at a time, so it lives as integer cents or as a decimal type that is exact and slower. A JSON id above 2 to the 53 parsed by JavaScript comes back as a different id, which is why Twitter's API sent every tweet id twice, once as a number and once as a string. A test that compares floats with equality passes on one machine and fails on another where the compiler fused a multiply and an add or the library summed in a different order.
The price of range is exactness, and every system that needs exactness has to buy it back explicitly: with integers for counting, decimals for rules written in base ten, and tolerances for comparing measurements. The one thing that never buys it back is formatting. Printing a total with two decimal places makes it look right and leaves the stored value exactly as wrong as it was.
A float is fast, one hardware instruction, and approximate. Use it for measurements, science and graphics, where the input is approximate anyway.
A decimal stores base-ten digits exactly to a chosen precision and is slower: on CPython 3.15 a decimal addition measured about four times the cost of a float addition. Use it where the rules are written in decimal, such as tax, interest and invoicing.
Integer cents, or the smallest unit of the currency, are exact and fast. Use them for balances and prices. Money in a float is the one choice that is always wrong.
- "0.1 + 0.2 not equalling 0.3 is a Python quirk." Every language using IEEE 754 doubles prints the same 0.30000000000000004. The cause is that 0.1 has no finite binary representation, not any language's implementation.
- "Floats are accurate to a fixed number of decimal places." They carry 15 to 17 significant digits, not decimal places. At 10 to the 16 the gap between doubles is 2, so the value has no fractional digits at all.
- "Double precision makes money safe." More bits make each error smaller and leave it there. Adding a cent a million times in a loop missed 10,000 in the seventh decimal place, and a report that must match the ledger to the cent will eventually not.
- "Rounding the display fixes the problem." The display is right and the stored value is still off. A total that prints as 10.00 can still fail an equality check against 10, because comparisons and sums use the stored value.
- "NaN is a missing value, like None." NaN compares false with everything, including itself. A NaN in a column silently breaks sorting, grouping and equality, where None would have raised an error at the first ordering comparison.
- Store money as integer minor units or a decimal type, never a float. Exactness cannot be restored after the fact.
- Compare floats with a tolerance chosen for the domain, never with equality. Two correct computations can differ in the last bit.
- Send identifiers and large integers as strings in JSON. Any consumer that parses numbers as doubles corrupts values above 2 to the 53.
- Check for NaN at the boundary where data enters a computation. One NaN spreads through every sum and comparison it touches.
Knowledge Check
A colleague reports that 0.1 + 0.2 == 0.3 is false in Python and proposes rewriting the module in Java to fix it. What happens?
- Java compares the two values exactly and prints true
- Java gives the same false, because both use IEEE 754
- Java stores the literals in decimal, so the sum is exact
- Java refuses to compile an equality test on two doubles
A metrics counter is kept in a 32-bit float and incremented once per request. What eventually happens?
- It wraps to a negative number after about 2.1 billion requests
- It becomes infinity once the count passes about 16 million
- It stops increasing at 16,777,216, since adding 1 rounds away
- It drifts by a fraction of a request each time, from the start
A job sums the same list of floats on one thread and then in parallel on eight threads. The totals differ in the last digits. Why?
- Float addition is not associative, and the order changed
- The threads raced and some of the additions were lost
- Each thread used a lower precision to save memory
- One of the cores has a faulty floating-point unit
A column of sensor readings contains one NaN. What happens when the program sorts the column and takes its maximum?
- Both calls raise an error that points at the NaN
- The NaN is sorted to the end and max skips over it
- The results depend on where the NaN happened to sit
- The NaN is treated as zero in both of the calls
Which representation fits each case: a temperature sensor, an account balance, and a tax rule defined as rounding at the third decimal place?
- Float for all three, since doubles have 15 digits
- Float, integer cents, and a decimal type
- Decimal, float, and integer cents
- Integer cents for all three, rounding each value
You got correct