1.8 KiB
1.8 KiB
| title | tags | created | sources | ||||
|---|---|---|---|---|---|---|---|
| Python Float Formatting — Equality Trap with Percentages |
|
2026-05-18 | daily/2026-05-18.md |
Python Float Formatting — Equality Trap with Percentages
The Problem
Binary floating-point arithmetic breaks integer-equality checks on values that look whole:
1.1 * 10 # → 11.000000000000002
(1.1 * 10) % 1 # → 2e-15 (not 0.0!)
(1.1 * 10) % 1 == 0 # → False ← BUG
A common context where this bites: formatting percentage strings.
def fmt_pct(value):
"""Format 1.1 → '1.1%', 2.0 → '2%'"""
pct = value * 100
if pct % 1 == 0: # WRONG — floats lie here
return f"{int(pct)}%" # never reached for 1.1
return f"{pct:.2f}%" # outputs "1.10%" instead of "1.1%"
Symptom: Values like 1.1, 3.3, 6.6 produce "1.10%", "3.30%", "6.60%" instead of "1.1%", "3.3%", "6.6%".
Root Cause
IEEE 754 double precision cannot represent most decimal fractions exactly. Multiplying by 100 amplifies the error. The % 1 == 0 check compares against exact zero — which the fractional residue never is.
Fix
Format to a fixed number of decimal places first, then strip trailing zeros:
def fmt_pct(value):
pct = value * 100
formatted = f"{pct:.10f}".rstrip("0").rstrip(".")
return f"{formatted}%"
Or use Decimal for exact arithmetic when the source is user-entered text:
from decimal import Decimal
def fmt_pct(value: str) -> str:
pct = Decimal(value) * 100
return f"{pct.normalize():f}%"
General Rule
Never use
float == integerorfloat % 1 == 0for "is this whole?" checks. Always format to fixed dp first, then apply string operations.
See Also
wiki/concepts/python-iso-z-suffix— another float/string representation trap