--- title: "Python Float Formatting — Equality Trap with Percentages" tags: [python, float, formatting, gotcha] created: 2026-05-18 sources: 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: ```python 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. ```python 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: ```python 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: ```python from decimal import Decimal def fmt_pct(value: str) -> str: pct = Decimal(value) * 100 return f"{pct.normalize():f}%" ``` ## General Rule > Never use `float == integer` or `float % 1 == 0` for "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