Floating-Point & Numeric Bugs
Binary floats cannot represent many decimal fractions exactly. Financial totals, thresholds, and equality checks fail when code treats float as exact decimal arithmetic.
Recipe
Quick-reference recipe card - copy-paste ready.
from decimal import Decimal, ROUND_HALF_UP
price = Decimal("19.99")
qty = Decimal("3")
total = (price * qty).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
# Never: if a == b for floats
import math
if math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0):
...When to reach for this:
0.1 + 0.2 != 0.3breaks tests or billing- Pandas
float64aggregates disagree with spreadsheet totals - Sorting or binning produces off-by-one at boundaries
- JSON APIs return noisy float tails (
19.990000000002)
Working Example
from decimal import Decimal, ROUND_HALF_UP, getcontext
import math
# --- classic float surprise ---
assert 0.1 + 0.2 != 0.3 # True in IEEE 754 float
# --- money with Decimal ---
def line_total(unit_price: str, qty: int) -> Decimal:
price = Decimal(unit_price)
return (price * qty).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
assert line_total("19.99", 3) == Decimal("59.97")
# --- safe float comparison ---
def nearly_equal(a: float, b: float) -> bool:
return math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-12)
# --- pandas 2.2+ aware aggregation ---
import pandas as pd
df = pd.DataFrame({"amount": ["10.10", "20.20"]})
# BUG path: astype(float) early can still accumulate binary error at scale
df["amount_dec"] = df["amount"].map(lambda x: Decimal(x))
total = sum(df["amount_dec"], Decimal("0"))
assert total == Decimal("30.30")
# --- context for banking rules ---
getcontext().prec = 28
tax_rate = Decimal("0.0825")
subtotal = Decimal("100.00")
tax = (subtotal * tax_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
assert tax == Decimal("8.25")What this demonstrates:
- Float equality is unsafe;
math.isclosecompares with tolerance Decimalconstructed from strings preserves decimal intentquantizeapplies explicit rounding rules for money- pandas pipelines should delay float conversion or stay in Decimal until presentation
Deep Dive
How It Works
floatis IEEE 754 binary64; many decimal rationals have no exact binary representationDecimalstores base-10 coefficients with adjustable precision and rounding modesFractionhandles rational numbers exactly when values are rational- Integer cents (
int) avoids float entirely for money when scale is fixed
Type Picking
| Use case | Type | Why |
|---|---|---|
| Money, tax, invoices | Decimal or int cents | deterministic decimal rules |
| Science/engineering | float + isclose | performance, numpy ecosystem |
| Ratios like 1/3 | Fraction | exact rational |
| ML features | float32/float64 | tensor APIs expect floats |
Python Notes
# BAD Decimal from float - inherits binary error
Decimal(0.1) # Decimal('0.1000000000000000055511151231257827021181583404541015625')
# GOOD
Decimal("0.1")Gotchas
Decimal(float)constructor - imports float imprecision into Decimal. Fix:Decimal(str(x))or parse from string input.- Mixing Decimal and float in ops - raises
TypeErroror coerces unexpectedly. Fix: pick one type per boundary. - JSON serialization -
Decimalnot JSON-native. Fix: quantize to string or int cents in API layer. - numpy/pandas float64 sum order - non-associative sums differ by order. Fix: Kahan summation, Decimal pass, or integer cents.
- Floor/ceil on negatives -
int(-1.2)truncates toward zero, not floor. Fix:math.floor/math.ceil. - Display rounding vs stored value - UI shows 2 decimals, backend compares full float. Fix: quantize at comparison boundary.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Integer minor units (cents) | fixed currency scale | FX with many decimal places |
Decimal | human decimal rules | high-throughput ML inner loops |
float + epsilon | graphics/simulation | regulatory financial totals |
| Polars 1.x Decimal dtype | typed ETL decimal columns | simple scripts |
FAQs
Why does Python show 0.30000000000000004?
That is the closest binary64 representation. print shows shortest round-trip form; the value is not exactly 0.3.
Should APIs use float or string for money?
Prefer string decimal or integer minor units in JSON. If float is unavoidable, document tolerance and never use raw equality.
What rounding mode should billing use?
Depends on regulation; ROUND_HALF_UP is common for retail. Encode the rule in one function and test edge cases (0.5 ulp).
Is math.isclose enough for money?
No for equality of money. Use Decimal quantize or integer cents. isclose suits scientific floats.
How do pandas nullable Int64 columns help?
Store cents as Int64 in pandas 2.2+ to avoid float altogether until display formatting.
Does Polars handle Decimal?
Polars 1.x supports Decimal dtypes for ETL; convert to string/int for JSON APIs explicitly.
What about numpy.isclose vs math.isclose?
Both exist; numpy version handles arrays. Pick one module per codebase layer for consistency.
Can I use Fraction for money?
Possible for rational prices but awkward for decimal tax tables. Decimal matches human base-10 rules better.
Why do CSV imports drift totals?
Locale commas and implicit float parsing. Read as string, parse to Decimal, then quantize.
How do I test rounding?
Table-driven tests on half-cent boundaries, negative amounts, and three-way tax splits that must sum to invoice total.
Does PyTorch 2.6+ use float32 by default?
Many tensors default to float32. Do not use torch floats for ledger math; keep finance in Decimal/int paths.
When is binary float OK for currency display only?
When values are already quantized to cents and you only format for UI, but keep ledger in int cents end-to-end.
Related
- Data Pipeline Defects - dtype coercion side effects
- Encoding & Unicode Bugs - parsing locale number strings
- Debugging Refactor Snippets - float to Decimal refactors
- Domain Modeling - Money value objects
- Debugging Tools - inspect values in pdb
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.