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

1.8 KiB

title tags created sources
Python Float Formatting — Equality Trap with Percentages
python
float
formatting
gotcha
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 == 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