Float equality and NaN¶
Level: 201 · for Python programmers, especially ones coming from Rust or C
One line: 0.1 + 0.2 == 0.3 is False because both sides were rounded to binary and rounded differently; a tolerance only works if it scales with the numbers, which math.isclose does and sys.float_info.epsilon does not; and NaN is unequal to itself everywhere except inside a container, which checks identity first.
A float is an IEEE 754 double. No double equals one tenth, so the literal 0.1 holds the nearest one, and so do 0.2 and 0.3. Each literal was rounded on its own, and the sum of two rounded values is rounded once more. Whether that result lands on the same double as the rounded 0.3 is luck, and for this sum it misses by one step. == on floats is not broken. It compares two exact binary values and reports, correctly, that they differ.
Rust in Action walks through the same three problems in its section on comparing numbers: an equality test that fails, a tolerance test, and NaN. Python gives the same answers to all three, with three differences that are Python's own. There is no 32-bit float to make the first test pass by accident. Containers treat NaN differently from ==. And some operations raise an exception where Rust and C return a NaN or an infinity.
The run¶
Verified output of float_equality_and_nan_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. 0.1 + 0.2 IS NOT 0.3
0.1 + 0.2 0.30000000000000004
0.1 + 0.2 == 0.3 False
0.5 + 0.25 == 0.75 True halves and quarters are exact in binary
float.hex() 64 bits
0.1 + 0.2 0x1.3333333333334p-2 3fd3333333333334
0.3 0x1.3333333333333p-2 3fd3333333333333
One step apart in the last hex digit. The exact values show why:
0.1 is stored as 0.1000000000000000055511151231257827021181583404541015625
0.2 is stored as 0.200000000000000011102230246251565404236316680908203125
0.3 is stored as 0.299999999999999988897769753748434595763683319091796875
0.1 and 0.2 are each stored a little high; 0.3 a little low.
math.nextafter(0.3, math.inf) == 0.1 + 0.2 True
2. THE SAME SUMS IN 32 BITS
Python has no 32-bit float. struct.pack('>f', x) rounds a double
to the nearest one, which is enough to replay the arithmetic.
question left, 32 bits right, 32 bits in 32 bits in 64 bits
0.1 + 0.2 == 0.3 3e99999a 3e99999a True False
0.1 + 0.6 == 0.7 3f333334 3f333333 False True
The first question is True in 32 bits and False in 64; the second
is the other way round. Neither width is the accurate one: each
rounds, and each gets lucky on different numbers. (For both pairs
the double sum of the two 32-bit values is exact -- the program
asserts it with Fraction -- so rounding that sum once to 32 bits
is exactly what 32-bit addition does.)
3. A TOLERANCE HAS TO BE RELATIVE
0.2 - (0.1 + 0.1) is 0.0 in 64 bits, and 0.0 in 32
A difference of exactly zero passes any tolerance, and == too.
To test a tolerance, the two sides have to differ:
sys.float_info.epsilon 2.220446049250313e-16 the gap between 1.0 and the next double
math.ulp(2000.3) 2.2737367544323206e-13 the gap near 2000
math.ulp(2000.3) / sys.float_info.epsilon 1024.0
a = 1000.1 + 1000.2
a 2000.3000000000002
a == 2000.3 False
a - 2000.3 == math.ulp(2000.3) True one gap apart: the next double up
abs(a - 2000.3) <= sys.float_info.epsilon False no two doubles near 2000 are that close
math.isclose(a, 2000.3) True rel_tol=1e-09 scales with the numbers
math.isclose(1e-20, 0.0) False abs_tol defaults to 0.0
math.isclose(1e-20, 0.0, abs_tol=1e-12) True near zero, say what close means
4. EQUALITY IS NOT THE BITS
0.0 == -0.0 True and the bits are 0000000000000000 and 8000000000000000
nan = float('nan')
nan == nan False every comparison with NaN is False...
nan != nan True ...except !=, which is its negation
nan < 1.0 or nan >= 1.0 False not less, not greater-or-equal
math.isnan(nan) True the test that works
Containers ask 'is it the same object?' before they ask ==:
[nan] == [nan] True one object, twice: equal
nan in [nan] True
{nan: 'x'}.get(nan) 'x'
[float('nan')] == [float('nan')] False two objects: == decides, and says no
{float('nan'): 'x'}.get(float('nan')) None
math.nan is math.nan True a module constant is one object...
[math.nan] == [math.nan] True ...so this list compares equal
5. WITH A NaN IN IT, THE ORDER OF THE ARGUMENTS DECIDES
max(nan, 1.0) nan asks 1.0 > nan: False, so nan stays
max(1.0, nan) 1.0 asks nan > 1.0: False, so 1.0 stays
min(nan, 1.0) nan
min(1.0, nan) 1.0
sorted([3.0, nan, 1.0, 2.0]) [3.0, nan, 1.0, 2.0] not sorted, and no error
6. SOMETIMES PYTHON RAISES WHERE IEEE 754 GIVES A VALUE
math.sqrt(-42.0) ValueError IEEE 754 says NaN
1.0 / 0.0 ZeroDivisionError IEEE 754 says inf
1.0 % 0.0 ZeroDivisionError C's fmod says NaN
math.log(0.0) ValueError IEEE 754 says -inf
1e308 ** 2 OverflowError C's pow says inf
...and sometimes it hands the value back:
1e308 * 1e308 inf the same number as 1e308 ** 2, and no error
1e308 * 10 inf
math.inf - math.inf nan
math.inf * 0 nan
math.isfinite(1e308 * 10) False the check to make before trusting a result
(-42.0) ** 0.5 a complex, imaginary part 6.480741
-42.0 ** 0.5 -6.480741 ** binds tighter than the minus: -(42.0 ** 0.5)
What the run shows¶
Section 1: the sum misses by one double. float.hex() is the readable way to see the bits: 0x1.3333333333334p-2 against 0x1.3333333333333p-2, the same mantissa except the last hex digit. The 64-bit patterns from struct.pack('>d', ...) are 3fd3333333333334 and 3fd3333333333333, the bits Rust prints with to_bits(). Decimal(x) shows the exact value a float holds, and it explains the miss. 0.1 and 0.2 were each stored slightly high, 0.3 slightly low, and the errors did not cancel. math.nextafter(0.3, math.inf) confirms that the sum is the very next double above 0.3. 0.5 + 0.25 == 0.75 is True, because halves and quarters are stored exactly. The trouble is decimal fractions, not floating point in general.
Section 2: 32 bits changes the luck, not the accuracy. The book's version of the test passes for f32. Python has no 32-bit float, but struct.pack('>f', x) rounds a double to the nearest one, which is enough to replay it. 0.1f + 0.2f and 0.3f are both 3e99999a, so the 32-bit test passes. 0.1 + 0.6 == 0.7 does the opposite: True in 64 bits, False in 32 (3f333334 against 3f333333). Neither width is more trustworthy. Each rounds, and each is lucky on different sums. The replay rests on one condition, which the program asserts rather than assumes: the double sum of the two 32-bit operands is exact, so rounding it once to 32 bits is 32-bit addition. Fraction confirms that for both pairs. It is not true of every pair of 32-bit floats. Two operands of very different magnitude have an exact sum that needs more than 53 bits.
Section 3: a tolerance has to scale. The book's tolerance example subtracts 0.1 + 0.1 from 0.2, and that difference is exactly 0.0 in both widths. Zero passes any tolerance, and it passes == too, so the example never actually tests the tolerance. The pair 1000.1 + 1000.2 and 2000.3 does. sys.float_info.epsilon is 2.220446049250313e-16, the gap between 1.0 and the next double, and it means nothing at 2000. There the gap, math.ulp(2000.3), is 1024 times wider. The sum is exactly one gap from 2000.3, and an absolute test against epsilon rejects it, because no two different doubles near 2000 are that close. The test has become == with extra steps. math.isclose uses rel_tol=1e-09 by default, relative to the larger of the two magnitudes, and accepts. Its other default, abs_tol=0.0, is the trap in the opposite direction. Relative to zero, every tolerance is zero, so math.isclose(1e-20, 0.0) is False until you say what "close to zero" means for your data.
Section 4: equality is not the bits, in either direction. 0.0 == -0.0 is True, though the bit patterns differ. A NaN goes the other way: the same object, with the same bits, is unequal to itself. That is the IEEE 754 rule, and Python's language reference states it for float('NaN') and Decimal('NaN'). Every ordered comparison with NaN is False, and != is True because it is the negation of ==. math.isnan is the test that works. The container rows are Python's own. list.__eq__, in and a dict lookup each check is before ==, so a NaN compared with itself inside a container is found and equal. Two NaN objects, as two float('nan') calls make, are compared with == and are not. math.nan is a single module-level object, so [math.nan] == [math.nan] is True, which makes a test that passes for a reason nobody intended.
Section 5: with a NaN in the arguments, their order decides. max() keeps its first argument and replaces it only when a later one compares greater. Every comparison with NaN is False, so a NaN in first place is never replaced, and a NaN anywhere else is never chosen. max(nan, 1.0) is nan and max(1.0, nan) is 1.0. sorted() has the same problem across a whole list and reports nothing: [3.0, nan, 1.0, 2.0] comes back unchanged. The wrong answer is not even stable across releases. The same list with the NaN moved to the front sorts differently before and after 3.13, so that row is dated below rather than kept in the answer key.
3.11 3.12 3.13 3.14
sorted([nan, 3.0, 1.0, 2.0]) [nan, 1.0, 2.0, 3.0] [nan, 1.0, 2.0, 3.0] [1.0, nan, 2.0, 3.0] [1.0, nan, 2.0, 3.0]
sum([0.1] * 10) == 1.0 False True True True
The second row is float equality changing under your feet. 3.12 switched sum() of floats to compensated summation, so an equality test that failed on 3.11 passes on 3.12, with no change to your code.
Section 6: Python raises where IEEE 754 returns a value, but only in some places. math.sqrt(-42.0), 1.0 / 0.0, 1.0 % 0.0, math.log(0.0) and 1e308 ** 2 all raise, where the standard (and C, and Rust) return NaN, an infinity or -inf. The pattern is where Python chose to check. Division, %, ** and the math functions check their operands or their result. Plain *, + and - do not, so 1e308 * 1e308 is inf with no error: the same number that 1e308 ** 2 refused to produce. math.inf - math.inf is nan, silently. So a NaN or an infinity can still reach your data. It arrives through the operations that do not check. Testing values with math.isfinite and math.isnan is the Python spelling of the defence the book recommends. The last two lines are a precedence trap rather than a float rule. (-42.0) ** 0.5 is a complex number, because ** with a negative base and a fractional exponent returns one in Python 3. -42.0 ** 0.5 is -(42.0 ** 0.5), because ** binds tighter than unary minus, so it is a perfectly ordinary -6.480741.
If you are coming from Rust¶
Rust has the same doubles and makes different choices around them. It has f32, so the book's 32-bit test is a real test that passes, not a replay. It never raises for floating-point arithmetic. And it keeps NaN out of places that need a total order: f64 implements PartialEq and PartialOrd but not Eq or Ord, so sort() on a Vec<f64> does not compile, and neither does insert on a HashSet<f64>. Python accepts a NaN in sorted(), in a set and as a dict key, and gives back whatever falls out.
Python Rust
math.sqrt(-42.0) ValueError (-42.0_f64).sqrt() NaN
1.0 / 0.0 ZeroDivisionError 1.0_f64 / 0.0 inf
-42.0 ** 0.5 -6.48074069840786 -42.0_f64.sqrt() -6.48074069840786
max(nan, 1.0) nan f64::NAN.max(1.0) 1.0
max(1.0, nan) 1.0 1.0_f64.max(f64::NAN) 1.0
sorted([3.0, nan, 1.0, 2.0]) v.sort() error[E0277]: f64: Ord is not satisfied
[3.0, nan, 1.0, 2.0] v.sort_by(|a, b| a.total_cmp(b))
[1.0, 2.0, 3.0, NaN]
0.2 - (0.1 + 0.1) 0.0 (0.2_f32 - (0.1_f32 + 0.1_f32)).abs() 0.0
The third row is the same trap in both languages. A method call binds tighter than unary minus in Rust, as ** does in Python, so neither line takes the square root of a negative number. f64::max returns the non-NaN operand whichever side it is on, which is the order-independence Python's max() lacks. The Rust library covers the bits in What a float stores ↗, and goes through the book's section claim by claim in Comparing numbers, claims checked ↗.
If you are coming from C¶
C returns the IEEE 754 value and records the event: sqrt(-42.0) is NaN and sets the FE_INVALID flag, and 1.0 / 0.0 is inf and sets FE_DIVBYZERO. The flags are sticky and nothing traps by default, so a program that never checks fetestexcept() never knows. The C library shows that in A float exception is a flag ↗. Python took the other side for some operations only. The checked operations in section 6 turn the event into an exception you cannot ignore. The unchecked ones behave like C with the flags thrown away, because Python gives you no way to read them. The fpectl module that once tried was removed in 3.7.
If you are coming from ABAP¶
ABAP's f type is the same binary double, so 0.1 + 0.2 compared with 0.3 in f fields has the same problem. The usual ABAP answer is not a tolerance at all but a different type: p (packed decimal with fixed decimal places) and decfloat16 / decfloat34 store 0.1 exactly, which is what Decimal does here, and why SAP stores amounts as packed numbers rather than f. ABAP also leans toward Python's side on errors. Division by zero raises CX_SY_ZERODIVIDE rather than returning an infinity, overflow raises CX_SY_ARITHMETIC_OVERFLOW, and a square root of a negative number raises rather than returning NaN. ABAP documents one special case that neither Python nor IEEE 754 has: a zero dividend divided by zero gives 0. Check the arithmetic and exception rules for your release before relying on these names. (Not machine-checked — CI cannot run ABAP.)
Try it¶
- Search your code for
== 0.0,!= 0.0, and==against a float literal. For each hit, decide whether the value is computed or assigned. An assigned0.0compares fine. A computed one needsmath.isclosewith anabs_tolyou choose, and choosing it means knowing your data's units. - Find a tolerance constant in a project of yours, like
1e-9,EPSorsys.float_info.epsilon. Printmath.ulp()of the largest value that tolerance is ever applied to. If the gap is wider than the tolerance, the check is==in disguise, as in section 3. - Take a column of measurements with gaps in it, from a CSV where empty cells become
float('nan'). Computemax()over it in file order and in reversed order. If the answers differ, find the row that did it. - Put
math.isfiniteon the boundary of one pipeline you run. Assert it on every value that leaves a calculation, run the pipeline on real input, and count what trips it. Section 6 says theinfandnanvalues reached you through*,+or-, so look there first. - Run a test suite of yours under
python:3.11-slimandpython:3.12-slim. If a float assertion changes result between them, look for asum()of floats first: that is the change dated in section 5.
Practice¶
Eight one-liners, and the one that depends on the order. Write down each result (a value, or the exception it raises) before running anything. import math first.
0.5 + 0.25 == 0.750.1 * 3 == 0.3math.isclose(0.1 * 3, 0.3)math.isclose(1e-20, 0.0)float('nan') in [float('nan')]min(float('nan'), 0.0)2.0 ** 1024-0.0 == 0.0
Then: one line would give a different answer if its two arguments were swapped, though what it asks should not depend on their order. Which one? And on which two lines was == the right test?
Answers
Verified output of float_equality_and_nan_kata_py.py — regenerated by tools/run_examples.py, never hand-typed.
expression result note
------------------------------------------------------------------------------------------
0.5 + 0.25 == 0.75 True halves and quarters are exact in binary
0.1 * 3 == 0.3 False 0.30000000000000004 is the next double up
math.isclose(0.1 * 3, 0.3) True one gap apart, far inside rel_tol=1e-09
math.isclose(1e-20, 0.0) False relative to zero is zero; abs_tol is 0.0
float('nan') in [float('nan')] False two objects, so == decides, and says no
min(float('nan'), 0.0) nan asks 0.0 < nan: False, so nan stays
2.0 ** 1024 OverflowError ** checks for overflow; * does not
-0.0 == 0.0 True different bits, equal numbers
THE ONE THAT DEPENDS ON THE ORDER
Line 6. Swapped, min(0.0, float('nan')) is 0.0. min() only
replaces its answer when a later argument compares less, and
every comparison against a NaN is False, so whichever argument
comes first is kept. A minimum should not depend on the order
it was asked in, and with a NaN in it, it does.
THE TWO WHERE == WAS THE RIGHT TEST
Lines 1 and 8. 0.5, 0.25, 0.75 and both zeros are stored exactly,
so == compares the numbers you wrote. Line 2 is the same shape
as line 1 and is False, because 0.1 and 0.3 are not stored
exactly -- which is the whole reason line 3 exists.
See also¶
- Comparing an
intwith afloat: the first half of the same book section, where one side is an integer and the comparison is exact - The format mini-language:
-0.00from a number that was never negative, and thezoption that fixes it - What a float stores ↗ and Comparing numbers, claims checked ↗ in the Rust library: the bits, and the book's section checked claim by claim
- A float exception is a flag ↗ in the C library:
fenv.h, and the sticky flag that stands in for an exception - Machine numbers ↗ and Catastrophic cancellation ↗ in the math library: why the gaps between doubles grow, and what subtraction does to the digits that are left
- Floating-point arithmetic: issues and limitations ↗: the Python tutorial's own chapter on
0.1 math.isclose↗ and PEP 485 ↗: the function, and the argument for a relative default and a zeroabs_tol- Prompted by §2.3.3 "Comparing numbers" in Tim McNamara's Rust in Action (Manning, 2021). The examples and prose here are written from scratch, and none of the book's listings are reproduced. See RESOURCES.
0.1 + 0.2 != 0.3, and2.5.roundis3↗ — the Ruby side: A Ruby Float is the same 64-bit double as a Python floatinjectmakes the block's value the next accumulator ↗ — the Ruby side:injectfeeds each block result back in as the accumulator, so a block that ends in an assignment hands the next…