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.
Search across all documentation pages
Binary floats cannot represent many decimal fractions exactly. Financial totals, thresholds, and equality checks fail when code treats float as exact decimal arithmetic.
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.3 breaks tests or billingfloat64 aggregates disagree with spreadsheet totals19.990000000002)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:
math.isclose compares with toleranceDecimal constructed from strings preserves decimal intentquantize applies explicit rounding rules for moneyfloat is IEEE 754 binary64; many decimal rationals have no exact binary representationDecimal stores base-10 coefficients with adjustable precision and rounding modesFraction handles rational numbers exactly when values are rationalint) avoids float entirely for money when scale is fixed| 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 |
# BAD Decimal from float - inherits binary error
Decimal(0.1) # Decimal('0.1000000000000000055511151231257827021181583404541015625')
# GOOD
Decimal("0.1")Decimal(float) constructor - imports float imprecision into Decimal. Fix: Decimal(str(x)) or parse from string input.TypeError or coerces unexpectedly. Fix: pick one type per boundary.Decimal not JSON-native. Fix: quantize to string or int cents in API layer.int(-1.2) truncates toward zero, not floor. Fix: math.floor / math.ceil.| 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 |
That is the closest binary64 representation. print shows shortest round-trip form; the value is not exactly 0.3.
Prefer string decimal or integer minor units in JSON. If float is unavoidable, document tolerance and never use raw equality.
Depends on regulation; ROUND_HALF_UP is common for retail. Encode the rule in one function and test edge cases (0.5 ulp).
No for equality of money. Use Decimal quantize or integer cents. isclose suits scientific floats.
Store cents as Int64 in pandas 2.2+ to avoid float altogether until display formatting.
Polars 1.x supports Decimal dtypes for ETL; convert to string/int for JSON APIs explicitly.
Both exist; numpy version handles arrays. Pick one module per codebase layer for consistency.
Possible for rational prices but awkward for decimal tax tables. Decimal matches human base-10 rules better.
Locale commas and implicit float parsing. Read as string, parse to Decimal, then quantize.
Table-driven tests on half-cent boundaries, negative amounts, and three-way tax splits that must sum to invoice total.
Many tensors default to float32. Do not use torch floats for ledger math; keep finance in Decimal/int paths.
When values are already quantized to cents and you only format for UI, but keep ledger in int cents end-to-end.
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+.
Reviewed by Chris St. John·Last updated Jul 19, 2026