Embercode
✕
Variables and Data TypesLesson 11 of 16

45 min read

Floating-point comparisons in Python

Why this matters

A shop program adds up item prices, then checks whether the total is exactly 0.30 before it applies a discount. Sometimes the check fails, the discount is skipped, and the program prints no error. The cause is a floating-point number. That is the usual name for a number with a decimal point, stored in a fixed amount of memory. In Python the type is float. Comparing two float values does not always work the way comparing two whole numbers does, and a program that gets it wrong still runs and prints a number that looks right.

This lesson covers three things. It shows why 0.1 + 0.2 is not 0.3. It shows how to compare two decimal results with a tolerance, which is a small allowed gap between them, and how to handle money exactly with decimal. It also shows what the special value nan, which means "not a number", does in comparisons and in sorting. The integer lessons cover whole numbers. This one covers decimal numbers only.

The idea: a computer counts in twos

People write numbers in base 10. Each place is a power of ten: units, tens, hundreds, and after the point tenths, hundredths and so on. A computer stores a float in base 2, which is called binary. Each place is a power of two. Before the point the places are 1, 2, 4, 8 and so on. After the point they are one half, one quarter, one eighth, and so on.

Think of a ruler with marks only at halves, quarters and eighths. You can measure 0.5 exactly, because 0.5 sits on a mark. You cannot measure 0.1 exactly, because no mark sits there. You pick the nearest mark, which is a tiny bit away from 0.1. A float works like that ruler. Some decimal numbers land on a mark, such as 0.5, 0.25 and 0.75. Most do not, including 0.1, 0.2 and 0.3.

The module decimal shows the exact value a float holds. Passing a float to Decimal prints every digit of the stored value:

Python · editable
0.1 stored as: 0.1000000000000000055511151231257827021181583404541015625
0.2 stored as: 0.200000000000000011102230246251565404236316680908203125
0.1 + 0.2 stored as: 0.3000000000000000444089209850062616169452667236328125

The stored 0.1 is a little above 0.1. The stored 0.2 is a little above 0.2. Their sum is a little above 0.3, and it is not the same stored value that the literal 0.3 gives. The digits after the first few do not matter to a human, but they matter to ==.

Seeing the problem

Here is the smallest program that shows the surprise. It adds two decimal literals, then compares the result to a third literal.

Python · editable
0.1 + 0.2 = 0.30000000000000004
0.3       = 0.3
a == b ? False
a - b = 5.551115123125783e-17

Python prints 0.30000000000000004 for the sum. This is not a display bug. The sum really is one step away from the stored 0.3. The == operator asks whether two values are exactly the same, and here they are not. The output 5.551115123125783e-17 uses scientific notation: e-17 means the number is multiplied by 10 to the power minus 17, so the gap is about 0.00000000000000005.

Some simple-looking decimal arithmetic gives the same kind of surprise. The next program adds 0.1 ten times, and then counts up by 0.1 in a loop.

Python · editable
total = 0.9999999999999999
total == 1.0 ? False
steps = 20 x = 2.0000000000000004

The loop in the second half never reaches exactly 1.0. The value jumps over it. Without the steps < 20 limit, the loop would never stop, because once x is past 1.0 it only grows. Rounding errors add up at each step. The test x != 1.0 assumes the value can hit 1.0 exactly, and with decimal steps it often does not.

Notice what the loop did not need: the counter steps is a whole number, and whole-number arithmetic is exact. The exercises use that fact.

Comparing with a tolerance

A tolerance is a small allowed difference. Two values are treated as equal when their gap is no bigger than the tolerance. The standard library function math.isclose does this check. It takes two values and two tolerances. The abs_tol keyword is a fixed number such as 1e-9, which means 0.000000001. The rel_tol keyword is a fraction of the size of the numbers being compared. Relative tolerance works better when the numbers can be very large or very small.

Python · editable
0.1 + 0.2 vs 0.3: True
1.0 vs 1.1: False
0.0 vs 1e-12 with abs_tol: True
0.0 vs 1e-12 with default: False
1e15 vs 1e15 + 0.25: True
1e15 vs 1e15 + 1000: True
nan vs nan: False
inf vs inf: True

Read the lines one by one. 0.1 + 0.2 and 0.3 count as equal, because their gap is far below the tolerance. 1.0 and 1.1 do not, because their gap is 0.1, which is much bigger than the tolerance. The pair 0.0 and 1e-12 counts as equal only when abs_tol is set. The default has no absolute tolerance, and the relative check allows only rel_tol times the larger value. Here that is 1e-21, so comparing with zero needs abs_tol.

The two lines about 1e15 show why the relative part exists. At one quadrillion, a float cannot store steps smaller than 0.125, so 1e15 + 0.25 is only two steps above 1e15. A plain check such as abs(a - b) < 1e-9 would say these numbers differ, which is wrong for most purposes. The relative check accepts them. The same check also accepts 1e15 + 1000, because at this size the allowed gap is one million. That is the cost of a relative tolerance: you must choose the number so that the gap it accepts is still small for your data.

The nan line has its own rule, covered in the next section. The infinity line is True because math.isclose checks for equal infinities before it does any subtraction.

Tolerance matters when at least one value comes from arithmetic. It is not needed when you only compare whole numbers, and it is not a fix for money. For money, use whole numbers of the smallest unit, such as cents in an int, or decimal.Decimal. Decimal keeps decimal digits exactly, so Decimal("0.1") + Decimal("0.2") is exactly 0.3.

Python · editable
Decimal sum = 0.3
equals 0.3? True

The value is built from text, not from a float, so the error never enters. Decimal is slower than float, so it is for money and exact decimal values. For a physics or graphics calculation, float with a tolerance is the normal choice.

A worked example: prices in cents

A common task is to turn a price such as 4.35 dollars into a whole number of cents, so the program can compare and store it exactly. The first idea is to multiply by 100 and cut off the decimal part with int. The program below tries that idea, and it also tries round, which rounds to the nearest whole number.

Python · editable
4.35 -> int 434 , round 435
0.57 -> int 56 , round 57
19.99 -> int 1998 , round 1999

The int version is one cent short for all three prices. The reason is the stored value. 4.35 * 100 gives 434.99999999999994, which is a little below 435, so cutting off the decimal part gives 434. 0.57 * 100 gives 56.99999999999999, so int gives 56. 19.99 * 100 gives 1998.9999999999998. The round version gives the cents the shop meant.

The exact approach avoids the float altogether. Build the price from text with Decimal, multiply, and convert to int at the end:

Python · editable
4.35 -> cents 435
0.57 -> cents 57
19.99 -> cents 1999

Each result is exact, because Decimal never stored the price as a binary fraction.

nan and infinity

Some float operations produce special values that are not numbers in the usual sense. Dividing a non-zero number by zero gives an error in Python, not infinity as in some other languages, as the mistakes section shows. The values float("inf") and float("nan") can be created on purpose. The functions math.sqrt and math.log do not return them for bad input: math.sqrt(-1) and math.log(0) raise ValueError. Arithmetic on infinity can still give nan, for example when you subtract one infinity from another.

Python · editable
nan = nan
nan == nan ? False
nan != nan ? True
math.isnan(nan) ? True
nan < 5 ? False
nan > 5 ? False
inf - inf = nan
-0.0 == 0.0 ? True

The rule to remember is that nan is not equal to anything, including itself. nan == nan is False. Every <, >, <= and >= comparison with nan is also False. This is the rule that catches people. A check such as if result == float("nan"): is never true, because result == anything is false when result is nan. The correct check is math.isnan(result), which returns True for nan only.

The != test gives True for nan != nan, because it is the opposite of ==. Some programmers use x != x to find nan. It works, but math.isnan says what it means, so use that.

The values -0.0 and 0.0 are two different bit patterns for zero. The == operator treats them as equal, as the last line shows.

Sorting with nan is a problem, because the < operator gives False in both directions for nan, so the sort has no defined place for it. The next program sorts a list with nan in it, and then removes the nan values first.

Python · editable
sorted with nan: [3.5, nan, -1.0, 2.0]
sorted without nan: [-1.0, 2.0, 3.5]

The first line shows the problem. The list is not sorted, and the nan sits in the middle. The second line filters out the nan values first, then sorts. Decide what a nan means in your problem, and remove it or report it before you sort.

nan spreads. Any arithmetic with a nan input gives nan, so one bad value can turn a whole total into nan. Check for it at the point where a value enters your program, not at the end when the result is already useless.

Common mistakes

The most common mistake is writing == between two computed float values. The program prints False with no error, and the bug can stay hidden until a test uses numbers that happen to line up. Use math.isclose for computed values, or decimal.Decimal for exact money.

The second mistake is comparing with float("nan") using ==. This runs and is never true, so use math.isnan instead.

The third mistake is using an absolute tolerance on numbers of any size. An absolute tolerance of 1e-9 is fine for values near 1. For values near 1e15, the spacing between neighbouring float values is 0.125, so an absolute tolerance smaller than the spacing can never pass for two values that are one step apart. Use a relative tolerance, or both, as math.isclose does with rel_tol and abs_tol.

The fourth mistake is dividing by zero with floats. In Python, 0.0 / 0.0 and 1.0 / 0.0 both raise an error, and the program stops:

print(1.0 / 0.0)
Traceback (most recent call last):
  File ".../divide.py", line 1, in <module>
    print(1.0 / 0.0)
          ~~~~^~~~~
ZeroDivisionError: division by zero

The message names the problem. Check the divisor before the division, as the division by zero lesson shows.

The fifth mistake is assuming a float is the same as a float32. Python's float is a 64-bit value, and a 32-bit value needs the ctypes module. Storing 0.1 in a 32-bit value gives a different number:

Python · editable
0.10000000149011612
False

The 32-bit value is not 0.1 either. It is the nearest 32-bit binary fraction, which is further from 0.1 than the 64-bit value. Use float unless a library or a file format asks for 32 bits.

The last mistake is trusting a tolerance without deciding its size. A tolerance of 1e-9 is not a fact about your problem. Choose it from what the numbers mean. A distance in metres might accept a millimetre. A probability might need a much smaller value. Write the tolerance as a named constant, so the reader can see the choice.

Practice

Work each exercise yourself before you open the answer. Predict the output on paper first.

  1. Without running the code, predict the output of print(0.5 + 0.25 == 0.75) and print(0.1 * 3 == 0.3). Then write a reason for each answer.
  2. Write a function is_close(a, b) that returns True when the absolute gap between a and b is less than 1e-9. Test it with 0.1 + 0.2 and 0.3, with 1.0 and 1.1, and with two equal infinities.
  3. Rewrite this loop so that it always runs exactly ten times and always reaches exactly 1.0 at the end:
x = 0.0
while x != 1.0:
    x += 0.1
  1. Predict the result of float("nan") < float("inf"), and explain why sorted cannot give a reliable order when the list has a nan in it.
  2. This program is meant to report when result is nan, but it never does. Find the bug and fix it.
result = float("nan")
if result == float("nan"):
    print("result is nan")

Each exercise has an answer below a "Show the answer" line. Try each one first, then read the answer.

  1. The first line prints True. The numbers 0.5, 0.25 and 0.75 are all exact in binary, because each is a sum of halves and quarters, so the sum is exact too. The second line prints False. The product 0.1 * 3 gives 0.30000000000000004, which is not the stored value of 0.3.

Show the answer

print(0.5 + 0.25 == 0.75)
print(0.1 * 3 == 0.3)

The output is True and then False.

  1. The check compares the gap with the tolerance. Two equal infinities need a separate check, because their gap is nan.

Show the answer

import math
 
def is_close(a, b):
    return math.isclose(a, b, rel_tol=0.0, abs_tol=1e-9)
 
print(is_close(0.1 + 0.2, 0.3))
print(is_close(1.0, 1.1))
print(is_close(float("inf"), float("inf")))

The output is True, False and True. The math.isclose function checks for equal infinities before it subtracts, so the last line is True. A version with abs(a - b) < 1e-9 would give False, because inf - inf is nan.

  1. Use a whole-number counter and divide only once per step.

Show the answer

for i in range(1, 11):
    x = i / 10
    print(i, x)

The counter is exact, so the loop always runs ten times. The last line prints 10 1.0. The division 3 / 10 gives the nearest stored value to 0.3, which is the same stored value the literal 0.3 gives, so 3 / 10 == 0.3 is True.

  1. The result is False. Every comparison with nan is False, so the sort cannot tell where the nan belongs. The result of sorted on such a list depends on the order of the items, and it is not reliable.

Show the answer

print(float("nan") < float("inf"))

The output is False.

  1. The check uses ==, and nan == anything is False, including nan == nan. Replace the test with math.isnan.

Show the answer

import math
 
result = float("nan")
if math.isnan(result):
    print("result is nan")

The output is result is nan.

What you now know

  • A float stores a decimal number in base 2, so many decimals such as 0.1 and 0.3 are stored a little off.
  • 0.1 + 0.2 == 0.3 is False, and loops that add 0.1 may skip the exact value they test for.
  • Compare computed float values with math.isclose, using abs_tol for values near zero and rel_tol for large values.
  • For exact decimal money, build the value with decimal.Decimal from text, or use whole numbers of the smallest unit.
  • nan is not equal to anything, including itself, so test for it with math.isnan.
  • Dividing a float by zero raises ZeroDivisionError, and sorting a list with nan in it gives no reliable order.
Practice this topic: coding problems for Variables and Data Types →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Big integers in Python