Python Float Comparison: How to Compare Floats Correctly
python float comparison: Learn why direct float equality fails and how to compare floats reliably using tolerance, math.isclose, and practical testing strategies.
Comparing floats with == in Python can produce surprising results. For example, 0.1 + 0.2 == 0.3 evaluates to False. This is not a bug in Python; it is a consequence of how binary floating-point arithmetic represents decimal fractions. When you need to decide whether two floats are equal, python float comparison requires a tolerance-based approach.
Why Direct Equality Fails for Floats
Floating-point numbers are stored as binary fractions. A decimal like 0.1 cannot be represented exactly in binary, so Python stores an approximation. When you perform arithmetic, rounding errors accumulate. The expression 0.1 + 0.2 produces a value that is slightly larger than 0.3, so the == operator returns False. This behavior is not unique to Python; it appears in any language that uses IEEE 754 binary floating-point arithmetic.
print(0.1 + 0.2) # 0.30000000000000004 print(0.1 + 0.2 == 0.3) # False
The exact value of 0.1 + 0.2 depends on the floating-point representation, but the key point is that you cannot rely on exact equality for most computed floats. Even simple operations like multiplication or division can introduce tiny errors.
The Role of Tolerance in Float Comparison
Instead of asking whether two floats are exactly equal, you should ask whether they are close enough. The standard approach is to define a tolerance—a maximum allowed difference. If the absolute difference between two values is less than or equal to that tolerance, you treat them as equal.
def is_close(a, b, tolerance=1e-9): return abs(a - b) <= tolerance
This simple function works for many cases, but it has a limitation: it uses an absolute tolerance. For very large numbers, a difference of 1e-9 is negligible, but for very small numbers, it may be too large. For example, 1e-12 and 2e-12 differ by 1e-12, which is less than 1e-9, so they would be considered equal even though they differ by a factor of two.
Using math.isclose for Relative and Absolute Tolerance
Python's math.isclose function provides a robust way to compare floats. It accepts both relative and absolute tolerances, and it handles edge cases like zero and infinity correctly. The default behavior uses relative tolerance, which scales with the magnitude of the numbers being compared.
import math print(math.isclose(0.1 + 0.2, 0.3)) # True print(math.isclose(1e-12, 2e-12)) # False (default rel_tol=1e-09) print(math.isclose(1e-12, 2e-12, abs_tol=1e-12)) # True
math.isclose uses the formula abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol). The default rel_tol is 1e-9, and the default abs_tol is 0.0. When comparing very small numbers, you should set abs_tol to a reasonable value. When comparing very large numbers, the relative tolerance automatically accounts for scale.
Comparing Floats in Practice: Unit Tests and Assertions
In unit tests, you often need to assert that a computed float matches an expected value. Python's unittest module provides assertAlmostEqual, which uses a fixed number of decimal places by default. For more control, you can use math.isclose inside your test assertions.
import unittest import math class TestFloatComparison(unittest.TestCase): def test_sum(self): self.assertTrue(math.isclose(0.1 + 0.2, 0.3)) def test_custom_tolerance(self): self.assertTrue(math.isclose(1e-12, 2e-12, abs_tol=1e-12))
If you are using pytest, you can use the approx fixture, which provides a similar tolerance-based comparison. The key is to avoid using == directly in assertions, because it will fail for many legitimate computations.
Handling Edge Cases: Zero, Infinity, and NaN
math.isclose handles several edge cases that a naive absolute-difference check would mishandle. For example, comparing a very small number to zero requires an absolute tolerance, because the relative tolerance would always be zero. math.isclose treats inf and -inf as equal only to themselves, and nan is never close to any value, including itself.
import math print(math.isclose(1e-300, 0.0, abs_tol=1e-200)) # True print(math.isclose(float('inf'), float('inf'))) # True print(math.isclose(float('inf'), float('-inf'))) # False print(math.isclose(float('nan'), float('nan'))) # False
These behaviors align with IEEE 754 semantics and prevent subtle bugs when your data contains special values.
Performance and Maintainability Considerations
Using math.isclose adds a small computational overhead compared to ==, but this is negligible in most applications. The real cost is in code clarity and correctness. A direct equality check is faster but often wrong. The maintainability benefit of math.isclose comes from its explicit tolerance parameters, which document the precision requirements of your comparison.
When performance is critical, such as in a tight numerical loop, you can precompute a tolerance and use a simple abs(a - b) <= tol check. However, you must ensure that the tolerance is appropriate for the magnitude of the values involved. In most cases, the overhead of math.isclose is acceptable, and the reduced debugging time outweighs the tiny performance cost.
Choosing the Right Comparison Strategy
The choice between absolute tolerance, relative tolerance, or math.isclose depends on the nature of your data. Use absolute tolerance when the values are expected to be in a known range, such as coordinates in a game or sensor readings. Use relative tolerance when the values can vary by orders of magnitude, such as in scientific computations. Use math.isclose when you need a balanced default that works for most cases.
For exact decimal arithmetic, consider using the decimal module or fractions.Fraction. These types represent numbers exactly but are slower and less memory-efficient than floats. They are appropriate for financial calculations or other scenarios where exactness is non-negotiable. For most engineering and scientific work, math.isclose with a well-chosen tolerance is the right tool.
from decimal import Decimal print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3')) # True
Remember that Decimal still has its own precision limits, but it avoids the binary representation issue. The decision ultimately comes down to the precision requirements and performance constraints of your application.