obsidian/wiki/concepts/python-float-formatting-comparison.md
2026-05-18 19:03:27 +01:00

64 lines
1.8 KiB
Markdown

---
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