Debugging Float Precision Issues
Bug report: "My test assert total == 1.0 fails but total prints as 1.0."
Pythonimport math total = sum([0.1] * 10) print(total) # 1.0 (displayed, but actually 0.9999999999999999) print(total == 1.0) # False — the bug # Fix: use fsum for accurate accumulation, isclose for comparison total = math.fsum([0.1] * 10) print(total == 1.0) # True
Use this decision process when a numeric result is "wrong" but looks right:
Progress:
- Step 1: Reproduce — print
repr(x)orf"{x:.20f}", notstr(x), to see hidden precision error - Step 2: Classify the bug (see decision tree below)
- Step 3: Apply the targeted fix
- Step 4: Re-verify with
math.isclose()— never re-add a raw==check - Step 5: Add a regression test using the same tolerance
Decision Tree
Symptom: equality comparison fails unexpectedly
→ Never compare floats with ==. Use math.isclose(a, b, rel_tol=1e-9).
- If comparing against zero,
rel_tolis useless (relative to 0 is always 0) — passabs_tolexplicitly:math.isclose(x, 0, abs_tol=1e-9).
Symptom: summing many floats drifts from the mathematically exact answer
→ Built-in sum() accumulates rounding error one addition at a time. Replace with math.fsum(), which uses Shewchuk's algorithm for a correctly-rounded result.
Pythonvalues = [1e16, 1.0, -1e16] sum(values) # 0.0 (wrong — 1.0 got lost) math.fsum(values) # 1.0 (correct)
Symptom: int(math.sqrt(n)) gives the wrong integer for large n
→ math.sqrt converts to float, which loses precision above ~2^53. Use math.isqrt(n) for exact integer results, no float roundtrip.
Pythonn = 10**30 int(math.sqrt(n)) ** 2 == n # False — precision lost math.isqrt(n) ** 2 <= n # True — isqrt is exact
Symptom: computing exp(x) - 1 or log(1 + x) for small x gives noisy/zero results
→ Catastrophic cancellation: subtracting two nearly-equal large-magnitude floats destroys precision. Use math.expm1(x) and math.log1p(x), which compute these directly without the cancellation step.
Pythonx = 1e-15 math.exp(x) - 1 # 1.1102230246251565e-15 (noisy, wrong-ish) math.expm1(x) # 1.0000000000000007e-15 (accurate)
Symptom: combinatorics or factorial results overflow or lose precision
→ Don't compute n! / (k! * (n-k)!) manually with floats. Use math.comb(n, k) / math.perm(n, k), which stay in exact integer arithmetic.
Symptom: distance/norm calculation is unstable for very large or small coordinates
→ Manual sqrt(x**2 + y**2) can overflow/underflow for extreme values. Use math.hypot(x, y, ...), which scales internally to avoid overflow.
Example 1: Financial-style running total that never quite hits zero
Input:
Pythonbalance = 0.0 for amount in [10.10, -3.30, -6.80]: balance += amount print(balance == 0.0) # expected True, got False
Diagnosis: repeated += on floats accumulates rounding error.
Output (fix):
Pythonimport math balance = math.fsum([10.10, -3.30, -6.80]) print(math.isclose(balance, 0.0, abs_tol=1e-9)) # True
Example 2: Integer square root for cryptography-scale numbers
Input:
Pythonn = 123456789123456789123456789123456789 root = int(n ** 0.5) print(root * root <= n) # False — overflowed float precision
Output (fix):
Pythonimport math root = math.isqrt(n) print(root * root <= n) # True print((root + 1) ** 2 > n) # True — confirms floor sqrt is correct
Example 3: Probability computed as ratio of factorials blows up
Input:
Pythonn, k = 1000, 500 p = math.factorial(n) // (math.factorial(k) * math.factorial(n - k)) # works but wastefully computes huge intermediate factorials
Output (fix):
Pythonimport math p = math.comb(n, k) # same result, computed more efficiently, no huge intermediates
- Treat
==on floats as a code smell during review — always ask "should this beisclose?" - When precision matters near zero, always set
abs_tolexplicitly inisclose; the defaultabs_tol=0.0makes the check meaningless there. - Prefer
math.fsumoversumin any loop that accumulates more than a handful of floats, especially in financial or scientific accumulation code. - Prefer
math.isqrt,math.comb,math.permover float-based equivalents whenever inputs are integers — they avoid the float precision ceiling (~2^53) entirely. - For values near a singularity (x near 0 in exp/log), reach for
expm1/log1pbefore assuming a bug elsewhere.
- Don't "fix" a float bug by rounding with
round(x, 2)— this masks the symptom without addressing accumulated error, and can reintroduce bugs at different scales. - Don't use
math.isclose()with defaults when comparing to zero — it will always returnFalsefor any nonzeroasince relative tolerance scales with the compared values. - Don't convert large integers to float for sqrt/pow just for convenience — verify the magnitude first (
n.bit_length() > 53is a red flag) and switch tomath.isqrt. - Don't assume
math.fsumis needed everywhere — for small lists of similar-magnitude numbers, the overhead isn't worth it; reserve it for precision-sensitive accumulation.