Tutorial

Python math.isclose

6 min read

In this comprehensive Python tutorial, we’ll explore the intricacies of comparing float values, shedding light on why relying on the ‘==’ operator for such comparisons can often yield unexpected outcomes. We’ll then introduce a robust solution: Python math.isclose function.

When delving into the realm of scientific computations in Python, one frequently encounters float values. However, comparing these values can be a daunting task due to the idiosyncrasies of how computers represent them.

Prepare to enhance your Python programming skills by mastering the art of precise float comparison. Let’s embark on this journey together, with ‘Python math.isclose’ as our guiding beacon.

The problem with using == for float comparison

Internally, Python represents a float value using a finite binary representation. If we run the following code to print 0.1,

print(0.1)

We get the output

output of float 0.1 print

However, if we want to see more of the fractional part, we can display it with 30 decimal places of precision.

print(f"{float(0.1):.30f}")

we will get more details about the fractional part.

output of 0.1 formatted as a string with a precision 30 decimal places

Now we can see that internally, 0.1 is not represented as an exact value because it is represented internally using binary. Binary can only give an exact representation if the fractional number is a power of two or a sum of powers of two. For example:

# print 0.5 formatted as a string with 30 digits 
# after the decimal place
print(f"{float(0.5):.30f}")

# evaluate 0.5+0.25 and print the result formatted 
# as a string with 30 digits after the decimal place
print(f"{float(0.5+0.25):.30f}")

# evaluate 0.5+0.125 and print the result formatted 
# as a string with 30 digits after the decimal place
print(f"{float(0.5+0.125):.30f}")

The output is as follows:

display powers of two or sum of powers of two with a precision 30 decimal place

From the output, we see that in this case, the representation is exact.

However, Python can only represent float values like 0.1 approximately.

This raises a problem when trying to carry out comparisons using the == operator. The following code snippet illustrates the issue.

expr = 0.5 + 0.125
num = 0.625

print(f"0.5 + 0.125 (to 30dp) gives {expr:.30f}")
print(f"0.625 (to 30 dp) gives {num:.30f}")
print(f"expr==num evaluates to {expr==num}")

The output we get is:

output for exact comparison with == returns true

From our output, we can see that the internal representation of 0.625 and the sum 0.5 + 0.125 are the same. So, comparison returns True.

The situation changes when we work with float values that are approximately represented internally, as the next code snippet shows.

expr = 0.1 + 0.2
num = 0.3

print(f"0.1 + 0.2 (to 30dp) gives {expr:.30f}")
print(f"0.3 (to 30 dp) gives {num:.30f}")
print(f"expr==num evaluates to {expr==num}")

The output is as follows:

display of comparison using == on inexact values of float

The comparison returns False since the sum stored in expr is represented differently from the value stored in num, even if we intended to hold the same value of 0.3 in both variables.

The approximate representation of float values make comparing them using the == operator unpredictable. Serious bugs could result if a situation similar to the above scenario occurred in our code. However, Python provides an alternative way that allows us to compare float values without running into this kind of bug.

The solution: Python  math.isclose function

Python version 3.5 introduced the math.isclose function to address this challenge that arises when float values are compared. Instead of checking if the two numbers compared are the same, math.isclose checks if they are close to each other within a specified tolerance value.

The math.isclose Python function is called like so:

math.isclose(a, b, rel_tol, abs_tol)

Each parameter is explained below:

  • a : The first value to compare for closeness (Required)
  • b : The second value to compare for closeness (Required)
  • rel_tol : The relative tolerance. The maximum allowed difference between a and b , relative to the bigger value compared. It has a default value of 1e-09 (Optional)
  • abs_tol : The absolute tolerance. The maximum allowed difference between a and b . It has a default value of 0.0

The function carries out the following calculation when doing comparisons:

abs_a, abs_b = abs(a), abs(b)
abs_tol_rel = rel_tol * max(abs_a, abs_b)

# The comparison...
abs(a - b) <= max(abs_tol_rel, abs_tol)

If a and b are close, the comparison returns True.

Examples of using Python math.isclose for float comparison

In the previous sections, we discussed the challenges of comparing float values using the equality operator, and how we could make use of an alternative, math.isclose, to check if two float numbers are close enough within a specific tolerance value. In this section, we will dive into examples of making use of math.isclose.

Python Comparing two float values with default tolerances

The math.isclose function is defined with a default  rel_tolvalue of 1e-09 and a default abs_tol value of 0.0. The next example will make use of these default tolerance settings to compare two float values.

from math import isclose

a = 0.1 + 0.1 + 0.1
b = 0.3

print(isclose(a, b))  # Output: True

In the example, we used math.isclose to check if the sum 0.1 + 0.1 + 0.1 and the float literal 0.3 are close based on the default settings. The code prints out True which means that their internal representations are as close as allowed by the default tolerance values.

On the other hand, we will get a return value of False if we use isclosewith default tolerance values to check if 1e-10 and 1e-11 are close.

result = isclose(1e-10, 1e-11)

print(result)  # Output: False

From the tolerance formula discussed in the section that defines math.isclose, the absolute difference between 1e-10 and 1e-11 should be smaller than max(rel_tol * max(1e-10, 1e-11), abs_tol).

The absolute difference between 1e-10 and 1e-11 (a value of 9e-11) is not smaller than the value returned by max(rel_tol * max(1e-10, 1e-11), abs_tol) (which is 1.0000000000000001e-19).

Therefore, isclose returns False for the comparison between 1e-10 and 1e-11using the default tolerance values. Thus, the current tolerance values are not sufficient to consider 1e-10 and 1e-11 close.

Python Comparing two float values with custom tolerance values

The default tolerance values work for many situations.

However, different values can be passed to math.isclose to suit specific requirements.

Let’s explore examples that pass different values for rel_tol and abs_tol.

Hers an example that compares two values 1000.0 and 1001.0 using rel_tol value 1e-5. Running the code snippet will show how the comparison plays out.

from math import isclose

result = isclose(1000.0, 1001.0, rel_tol=1e-5)

print(result)  # Output: False

Here, math.isclose returns False because the absolute difference between 1000.0 and 1001.0 relative to the magnitude of the bigger number 1001.0does not fall within the specified relative tolerance.

This next example finds out if the sum 0.1 + 0.2 is within 1e-10 of 0.3.

from math import isclose

result = isclose(0.1+0.2, 0.3, abs_tol=1e-10)

print(result)  # Output: True

math.isclose returns True since the absolute difference between the internal representation of the result of 0.1 + 0.2 and the internal representation of 0.3  is smaller than1e-10 (the abs_tol).

In conclusion math.isclose offers a solution arising when trying to compare float values. Instead of directly checking for equality, the function checks if the absolute difference of the compared values falls under a tolerance value, effectively overcoming rounding errors and precision issues.

Understanding the use of math.isclose is crucial if we want to avoid bugs when carrying out float comparisons in our code. If you liked this tutorial, please consider checking out other tutorials like this on our website that can help you in your coding journey.