Skip to content

Python Floating-Point Precision: Why Your Math is Wrong (And How to Fix It)

The “Broken Calculator” Problem

When I first started learning Python, I wrote a simple calculation:

>>> 0.1 + 0.2 == 0.3
False

What? That’s not right. Let me check what Python actually calculated:

>>> 0.1 + 0.2
0.30000000000000004

I thought I broke something. But then I saw a Reddit post where someone calculated the cubic root of 64:

>>> 64 ** (1/3)
3.9999999999999996

They expected exactly 4.0, not this weird 3.999… number. This wasn’t just me - Python math seemed broken.

Let me see what Python actually stores when I type 0.1:

>>> print(f"{0.1:.60f}")
0.100000000000000005551115123125782702118158340454101562500000

That’s not 0.1 at all. Python thinks I typed this long messy number.

Why Computers Can’t Do Simple Math

After digging into this, I found that the problem isn’t Python - it’s how computers represent numbers.

We humans use base-10 (decimal). We write 0.1 as one tenth. Simple.

Computers use base-2 (binary). They only have 0s and 1s. Here’s what happens when I try to represent 0.1 in binary:

Decimal: 0.1
Binary: 0.0001100110011001100110011001100110011001100110011...

The pattern “0011” keeps repeating forever. Just like 1/3 = 0.333… in decimal, some decimal fractions become infinite in binary.

Computers can’t store infinite digits. They have to cut off somewhere. When Python stores 0.1, it actually stores:

Binary: 0.0001100110011001100110011001100110011001100110011001101
└──────────────────────────────────────────────────┘
52-bit precision limit

That last digit gets rounded, and that tiny error propagates through calculations.

This isn’t a bug. It’s the IEEE 754 standard - a specification that tells computers how to store floating-point numbers.

IEEE 754: Understanding Binary Floating-Point

Python stores floating-point numbers using the IEEE 754 double-precision format. Here’s what that looks like:

┌─────────┬──────────┬─────────────────────────────────────┐
│ Sign │ Exponent │ Mantissa │
│ (1 bit) │ (11 bits)│ (52 bits) │
└─────────┴──────────┴─────────────────────────────────────┘

Three parts:

  • Sign bit: 0 for positive, 1 for negative
  • Exponent: Like scientific notation (tells us where the decimal point goes)
  • Mantissa: The actual digits (precision)

Double precision means 64 bits total. That gives Python about 15-17 decimal digits of precision.

Let me check my system’s float precision:

>>> import sys
>>> print(f"Decimal digits of precision: {sys.float_info.dig}")
Decimal digits of precision: 15

So Python guarantees about 15 significant decimal digits. Beyond that, rounding errors show up.

This is why 0.1 + 0.2 gives me 0.30000000000000004 - the error is in the 17th digit, beyond the guaranteed precision.

Common Floating-Point Pitfalls

I’ve run into several situations where floating-point precision causes real problems.

Pitfall 1: Direct Comparison with ==

When I compare floats directly, I get wrong results:

>>> 0.1 + 0.2 == 0.3
False

This fails because of the tiny precision error. Instead, I use tolerance-based comparison:

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True

The math.isclose() function checks if two numbers are close enough within a tolerance.

I can also specify my own tolerance:

>>> abs((0.1 + 0.2) - 0.3) < 1e-9
True

Pitfall 2: Accumulation Errors

When I add the same number repeatedly, errors compound:

>>> total = 0
>>> for _ in range(10):
... total += 0.1
>>> print(total)
0.9999999999999999

I expected 1.0, but got 0.999… Each addition added a tiny error, and those errors accumulated.

Here’s what happened step by step:

0.1
+ 0.1 = 0.2 (theoretically)
= 0.2000000000000000111... (in binary)
+ 0.1 = 0.3 (theoretically)
= 0.3000000000000000444... (in binary)
... after 10 additions = 0.9999999999999999

Pitfall 3: Currency Calculations

This is dangerous. Using floats for money can give wrong results:

>>> price = 19.99
>>> tax_rate = 0.0825
>>> total = price * (1 + tax_rate)
>>> print(f"${total:.2f}")
$21.64

But what’s the actual value?

>>> print(f"{total:.20f}")
21.63917499999999892308

The true value should be 21.639175. If I round this for financial calculations, I might charge the wrong amount or report incorrect totals.

Python Solutions: Float vs Decimal vs Fraction

Python gives me three main tools for handling numbers with precision requirements.

Solution 1: math.isclose() for Comparisons

For most comparison cases, math.isclose() works well:

>>> import math
>>> result = 0.1 + 0.2
>>> if math.isclose(result, 0.3):
... print("Equal within tolerance")
Equal within tolerance

I can adjust the tolerance for my needs:

>>> math.isclose(1.000001, 1.0, rel_tol=1e-6)
True
>>> math.isclose(1.000001, 1.0, rel_tol=1e-9)
False

Use math.isclose() for:

  • General floating-point comparisons
  • Testing scientific calculations
  • Checking if sensor readings are within expected range

Solution 2: decimal.Decimal for Financial/Math

When I need exact decimal representation (like money), I use the decimal module:

>>> from decimal import Decimal, getcontext
>>> getcontext().prec = 28 # Set precision
>>> price = Decimal('19.99')
>>> tax = Decimal('0.0825')
>>> total = price * (Decimal('1') + tax)
>>> print(total)
21.639175

Notice: I initialize Decimal with strings, not floats. If I use a float, I bring in the floating-point error:

>>> # WRONG - brings in float error
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> # RIGHT - exact representation
>>> Decimal('0.1')
Decimal('0.1')

Here’s my financial calculator example:

from decimal import Decimal, ROUND_HALF_UP
def calculate_invoice(subtotal, tax_rate, discount=0):
"""Safe financial calculation using Decimal"""
subtotal = Decimal(str(subtotal))
tax_rate = Decimal(str(tax_rate))
discount = Decimal(str(discount))
tax = subtotal * tax_rate
discounted = subtotal - discount
total = discounted + tax
# Round to 2 decimal places (cents)
return total.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
>>> total = calculate_invoice(100.00, 0.0825, 5.00)
>>> print(f"${total}")
$103.25

Use Decimal for:

  • Financial calculations
  • Currency operations
  • Legal/compliance requirements
  • When exact decimal representation matters

But there’s a performance cost. Decimal is slower than float:

>>> import timeit
>>> # Float - fast
>>> timeit.timeit('0.1 + 0.2', number=1000000)
0.03 seconds
>>> # Decimal - slower but precise
>>> timeit.timeit("Decimal('0.1') + Decimal('0.2')",
... setup='from decimal import Decimal',
... number=1000000)
0.8 seconds

Decimal is about 20-30x slower than float, but for financial calculations, correctness matters more than speed.

Solution 3: fractions.Fraction for Rational Numbers

When I need exact rational arithmetic, I use Fraction:

>>> from fractions import Fraction
>>> one_third = Fraction(1, 3)
>>> one_third * 3
Fraction(1, 1) # Exactly 1!
>>> one_third + one_third + one_third
Fraction(1, 1) # Perfect!

No rounding errors. Fractions are exact.

Use Fraction for:

  • Mathematical proofs
  • Exact rational arithmetic
  • Symbolic math
  • When denominators are important

Here’s a comparison:

TypePrecisionSpeedUse Case
float~15-17 digitsFastestGeneral, scientific
DecimalConfigurable20-30xFinancial, currency
FractionExact (rational)100xMath proofs, exact rational

Real-World Examples

Example 1: E-commerce Shopping Cart

For a shopping cart, I always use Decimal:

from decimal import Decimal
def calculate_cart(items):
"""Safe cart calculation with Decimal"""
subtotal = Decimal('0')
for item in items:
price = Decimal(str(item['price']))
quantity = Decimal(str(item['quantity']))
subtotal += price * quantity
return subtotal
# Bad: float
>>> 0.1 + 0.2 + 0.3
0.6000000000000001
# Good: Decimal
>>> Decimal('0.1') + Decimal('0.2') + Decimal('0.3')
Decimal('0.6')

Example 2: Scientific Measurements

For sensor data, I use math.isclose():

import math
def sensor_reading_valid(reading, expected):
"""Use isclose for sensor data"""
return math.isclose(reading, expected, rel_tol=1e-5)
>>> sensor_reading_valid(0.30000000000000004, 0.3)
True

Sensors have inherent imprecision, so a small tolerance is appropriate.

Example 3: Graphics/Game Development

For position comparisons, I define an explicit epsilon:

def positions_equal(pos1, pos2, epsilon=1e-6):
return abs(pos1 - pos2) < epsilon

Game positions are floats, and exact equality rarely matters.

Advanced: Machine Epsilon

I wanted to understand the limits of float precision, so I looked at machine epsilon:

>>> import sys
>>> sys.float_info.epsilon
2.220446049250313e-16

This is the smallest number that, when added to 1.0, produces a result different from 1.0.

Let me test this:

>>> 1.0 + 1e-16 == 1.0
True # Too small to matter
>>> 1.0 + 1e-15 == 1.0
False # Large enough to register

Machine epsilon tells me the smallest meaningful difference I can represent with a float. Any error smaller than this gets lost.

Best Practices Checklist

After working through all of this, here’s what I do:

DO ✓:

  • Use math.isclose() for float comparisons
  • Use Decimal for money/currency
  • Set explicit tolerance values
  • Document precision requirements
  • Initialize Decimal from strings
  • Understand my domain requirements

DON’T ✗:

  • Never compare floats with ==
  • Don’t use float for financial calculations
  • Don’t initialize Decimal with floats: Decimal(0.1) is wrong
  • Don’t ignore precision warnings
  • Don’t assume round() fixes everything

Summary

In this post, I explained why 0.1 + 0.2 != 0.3 in Python. The key points are:

  • Computers use binary (base-2), while we use decimal (base-10)
  • Some decimal fractions like 0.1 are infinite in binary
  • IEEE 754 double-precision provides ~15-17 decimal digits of accuracy
  • Use math.isclose() for float comparisons
  • Use Decimal for financial calculations
  • Use Fraction for exact rational arithmetic
  • Never compare floats with ==

The floating-point “error” isn’t a bug - it’s a fundamental limitation of how computers represent numbers. Understanding this helps me write more reliable code and choose the right tool for each job.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments