Floating-Point Numbers and Decimal Precision

Updated

September 7, 2026

Floating-Point Numbers and Decimal Precision

After reading this chapter, you will understand the IEEE 754 standard behind Python’s float, avoid floating-point representation traps, and know when to use decimal.Decimal and fractions.Fraction.

Mental model

Python’s float type is implemented as a standard C double—a 64-bit IEEE 754 binary floating-point number:

┌──────┬───────────────────────┬─────────────────────────────────────────────────┐
│ Sign │ Exponent (11 bits)    │ Fraction / Mantissa (52 bits)                   │
│ 1b   │ biased binary scale   │ normalized significant digits                   │
└──────┴───────────────────────┴─────────────────────────────────────────────────┘

Because numbers are stored in base-2 (binary fractions), decimal numbers like 0.1 (\(1/10\)) cannot be represented exactly in binary, just as \(1/3\) cannot be represented in a finite number of decimal digits (\(0.3333...\)).

In base 10: 1/10 = 0.1 (exact)
In base 2 : 1/10 = 0.00011001100110011... (infinite repeating binary sequence!)

Therefore, 0.1 + 0.2 produces 0.30000000000000004 instead of 0.3.


Minimal example

Save as float_precision.py:

# float_precision.py
import math
from decimal import Decimal

def main() -> None:
    # Standard float arithmetic
    f1 = 0.1
    f2 = 0.2
    f_sum = f1 + f2
    print(f"Float 0.1 + 0.2  : {f_sum:.20f}")
    print(f"f_sum == 0.3     : {f_sum == 0.3} (Trap!)")
    print(f"math.isclose(...) : {math.isclose(f_sum, 0.3)} (Correct comparison)")

    # Exact decimal arithmetic
    d1 = Decimal("0.1")
    d2 = Decimal("0.2")
    d_sum = d1 + d2
    print(f"\nDecimal sum      : {d_sum}")
    print(f"d_sum == Decimal('0.3'): {d_sum == Decimal('0.3')}")

if __name__ == "__main__":
    main()

Run via uv run python float_precision.py:

Float 0.1 + 0.2  : 0.30000000000000004441
f_sum == 0.3     : False (Trap!)
math.isclose(...) : True (Correct comparison)

Decimal sum      : 0.3
d_sum == Decimal('0.3'): True

Worked examples

Case 1: Financial calculations with decimal.Decimal

Never use float for currency, invoices, or billing. Always use Decimal initialized with strings:

# billing_calculator.py
from decimal import Decimal, ROUND_HALF_UP

def calculate_invoice(subtotal_cents: str, tax_rate: str) -> None:
    subtotal = Decimal(subtotal_cents)
    tax = (subtotal * Decimal(tax_rate)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    total = subtotal + tax

    print(f"Subtotal : ${subtotal:>8}")
    print(f"Tax ({tax_rate}): ${tax:>8}")
    print(f"Total    : ${total:>8}")

if __name__ == "__main__":
    calculate_invoice("149.95", "0.0825")

Run:

uv run python billing_calculator.py

Output:

Subtotal : $  149.95
Tax (0.0825): $   12.37
Total    : $  162.32

Case 2: Exact rational arithmetic with fractions.Fraction

When you need exact symbolic fraction arithmetic without rounding:

# rational_math.py
from fractions import Fraction

def main() -> None:
    f1 = Fraction(1, 3)
    f2 = Fraction(1, 6)
    result = f1 + f2
    print(f"1/3 + 1/6 = {result} (Exact reduction to 1/2)")

    # Converting float to nearest fraction
    approx = Fraction(0.75)
    print(f"0.75 as Fraction: {approx}")

if __name__ == "__main__":
    main()

Run:

uv run python rational_math.py

Pitfalls

Pitfall 1: Initializing Decimal from a float literal

# Danger:
bad = Decimal(0.1)  # Decimal('0.1000000000000000055511151231257827021181583404541015625')

# Correct: Always pass strings to Decimal
good = Decimal("0.1")

Pitfall 2: Comparing floats with ==

Never compare floats directly using == unless checking against 0.0. Always use math.isclose(a, b, rel_tol=1e-9) or abs(a - b) < epsilon.


Exercises

  1. Write a function safe_float_equal(a: float, b: float, epsilon: float = 1e-9) -> bool that verifies float equality within tolerance.
  2. Calculate the compound interest of $10,000 at 5.5% annual interest compounded monthly over 10 years using decimal.Decimal.
  3. Demonstrate special float values: positive infinity (float("inf")), negative infinity (float("-inf")), and Not-a-Number (float("nan")). Check their behavior with math.isinf() and math.isnan().

Further reading

  • David Goldberg: What Every Computer Scientist Should Know About Floating-Point Arithmetic.
  • Python Standard Library: decimal module (Decimal fixed point and floating point arithmetic).
  • Python Standard Library: math.isclose documentation.