skip to content

When should you use math.isclose instead of == to compare two floats?

level: middleimportance: must knowfreq 60%

answer

  1. Exact equality asks the wrong question
  2. Tolerance, not bit identity
  3. The default tolerance is relative
  4. Relative windows collapse at zero
  5. abs_tol is the floor you set

basics

~10 s

Use math.isclose for any value produced by arithmetic, where rounding makes exact equality meaningless. Its default rel_tol of 1e-09 scales with the larger operand, so comparing against zero needs an explicit abs_tol.

solid answer

~40 s

`==` on floats is an exact bit comparison, which is right for a value you stored and wrong for a value you computed, because every operation may have rounded. `math.isclose(a, b)` instead asks whether the two are within a tolerance, and by default that tolerance is **relative**: `rel_tol=1e-09` means the gap must be at most a billionth of the larger magnitude. That scales correctly across magnitudes but degenerates at zero, where the window shrinks to nothing — `math.isclose(1e-09, 0.0)` is False. Comparing against zero therefore requires an explicit `abs_tol`, an absolute floor chosen from what the quantity means. Two edge behaviours are worth knowing: `math.isclose` treats two infinities of the same sign as close, and it never reports a nan as close to anything, including itself.

code

python · 11 lines
python
import math

total = 0.0
for _ in range(10):
    total += 0.1

print(total)                                  # 0.9999999999999999
print(total == 1.0)                           # False
print(math.isclose(total, 1.0))               # True
print(math.isclose(1e-09, 0.0))               # False: the window is zero wide
print(math.isclose(1e-09, 0.0, abs_tol=1e-06))  # True

go deeper

for a junior

Know that comparing computed floats with == is a bug pattern and that the standard library ships the fix. Remember the name math.isclose and that it takes optional rel_tol and abs_tol keyword arguments.

for a middle

You are expected to state the formula, name the defaults, and explain why the relative window collapses at zero so that comparisons against zero need abs_tol. Be able to say why the symmetric form beats a hand-written ratio check.

for a senior

Show that you choose tolerances from the domain rather than copying defaults, and that you sometimes fix the computation instead of the comparison — compensated summation, reordering, or moving to integer units. Explain how a badly chosen tolerance hides real regressions in a numeric test suite.

for a principal

Own the convention across a codebase: where tolerances are defined, whether they live beside the data that justifies them, and how numeric comparisons in tests are kept from becoming either flaky or so loose that they assert nothing. Decide when a quantity should stop being a float at all.

### Why exact equality is the wrong question Every binary64 operation is correctly rounded, meaning the result is the nearest double to the true answer. That per-operation error is tiny, but a chain of operations accumulates it, and two chains that are mathematically identical can land on different doubles. Asking `a == b` about such values asks whether two *different computations* happened to round the same way, which is not the question the code cares about. The question the code cares about is whether the values agree to within the precision the problem actually has. `math.isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0)` (added in Python 3.5 by PEP 485) answers that. It returns True when ``` abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) ``` Two properties fall out of that formula. **It is symmetric.** The tolerance uses the larger of the two magnitudes, so `isclose(a, b)` and `isclose(b, a)` always agree. Hand-rolled checks of the form `abs(a - b) / b < tol` are asymmetric and blow up when the denominator is zero; this is one of the main reasons to use the stdlib function rather than write your own. **It is relative by default.** `rel_tol=1e-09` means "agree to about nine significant digits", which is a sensible default because it scales: it is as meaningful for 1e-8 as for 1e12. But relative tolerance is defined in terms of magnitude, and at zero there is no magnitude. `math.isclose(1e-09, 0.0)` is False, and so is `math.isclose(1e-300, 0.0)`. Any comparison against zero — a residual, a difference, a balance that should have cancelled — needs `abs_tol` set to a floor that means something in the units of the problem. ### Choosing the tolerances The defaults are a starting point, not a law. Two rules of thumb: - Set `rel_tol` from the precision you actually have. Sensor readings good to four digits do not deserve a nine-digit comparison; a long iterative computation may need a looser one than the default. - Set `abs_tol` from the smallest difference that matters. If you are comparing quantities in kilograms and a gram is irrelevant, `abs_tol=1e-03` says exactly that, and it also rescues the near-zero case. Both can be given at once, and the check passes if *either* window is satisfied; that combination is the usual shape for code that must handle values spanning zero and large magnitudes in the same call. ### Special values `math.isclose(float('inf'), float('inf'))` is True and the negative pair likewise, because the difference of identical infinities is treated as agreement; a positive and a negative infinity are not close. Nan is never close to anything, including another nan, so `isclose` will not silently swallow a nan that leaked into a pipeline — but that also means it cannot be used to *detect* one. Use `math.isnan` and `math.isfinite` for that. ### Reducing the error before you compare Sometimes the better fix is to make the computation more accurate rather than the comparison looser. `math.fsum` sums an iterable of floats with exact partial sums and one final rounding, so it returns the correctly rounded total regardless of ordering. Since **Python 3.12** the builtin `sum` applies Neumaier compensated summation to floats, which is why `sum([0.1] * 10) == 1.0` is True on 3.12 through 3.14 while accumulating the same ten values in a hand-written `+=` loop still ends on 0.9999999999999999. Knowing that the builtin changed matters when you read older code that reached for `math.fsum` purely to fix a total, and it matters more when you write the loop yourself, because the loop got no such upgrade. ### What not to do Comparing `round(a, 2) == round(b, 2)` looks like a tolerance check and is not: it is a *bucketing* check, and two values a hair apart can straddle a bucket boundary and compare unequal no matter how close they are. Writing `abs(a - b) < 1e-09` without thinking is a fixed absolute tolerance, fine near zero and useless for large magnitudes where a billionth is far below one ulp. And chasing exactness by comparing string formatting of the two values just moves the rounding somewhere less visible.

  • What are the default tolerances, and what does the relative one actually mean?
    `rel_tol=1e-09` and `abs_tol=0.0`. The check passes when the absolute difference is at most `rel_tol` times the *larger* of the two magnitudes, or at most `abs_tol`, whichever window is wider. Using the larger magnitude is what makes the function symmetric, unlike a hand-written `abs(a - b) / b < tol`.
  • How does math.isclose treat infinity and nan?
    Two infinities of the same sign are reported as close; opposite signs are not. A nan is never close to anything, including itself, so `isclose` will not hide a nan that leaked into your data — but it cannot detect one either. Test for those explicitly with `math.isnan` and `math.isfinite`.
  • Why is `round(a, 2) == round(b, 2)` not an acceptable substitute?
    It buckets rather than measures. Two values a fraction apart can fall either side of a bucket boundary and compare unequal however close they are, while two values far apart inside one bucket compare equal. A tolerance check asks about the distance between the values, which is the thing you actually mean.

saying these in an interview costs you the question

  • Uses == on values produced by arithmetic
  • Thinks math.isclose has a nonzero abs_tol by default
  • Compares against zero with the default tolerances
  • Writes an asymmetric abs(a - b) / b check
  • Substitutes rounding both sides for a tolerance
  • Expects two nan values to be reported close

context