Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python Module 2 – Values, variables, numbers and strings

Floating-point surprises and how to handle them

Why 0.1 + 0.2 is not 0.3 in Python, how floats are stored in binary, and how to compare, add and round them safely with math.isclose, fsum and Decimal.

  • Beginner
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot

What you will learn

  • Explain why 0.1 + 0.2 != 0.3 using binary representation
  • Compare floats safely with math.isclose
  • Choose between float, decimal.Decimal and fractions.Fraction

Before you start

On this page

Ask Python for 0.1 + 0.2 and it answers 0.30000000000000004. Nothing is broken: C, Java and almost every other language store such numbers the same way and get the same result. Once you know what a float really holds, the surprises become predictable, and a few habits keep them out of your results.

What a float really stores

On almost every computer, a Python float is an IEEE 754 “double precision” number: a binary fraction with 53 significant bits and an exponent. Binary digits after the point stand for halves, quarters, eighths and so on, so a fraction can be stored exactly only if it is a sum of such pieces, which means its denominator, in lowest terms, is a power of 2. 0.75 is ½ + ¼ and is exact. 0.1 is 1/10, and 10 has a factor of 5, so in binary it never ends: 0.000110011001100… The float keeps the first 53 significant bits and rounds the rest away.

The values behind 0.1, 0.2 and 0.3 Python · stored_value.py
from decimal import Decimal

print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(0.5 + 0.25 == 0.75)  # halves and quarters are exact in binary

print(Decimal(0.1))  # the exact value stored for 0.1
print(Decimal(0.2))
print(Decimal(0.3))
print((0.1).as_integer_ratio())  # the same stored value as a fraction
print(2 ** 55)

Output

0.30000000000000004
False
True
0.1000000000000000055511151231257827021181583404541015625
0.200000000000000011102230246251565404236316680908203125
0.299999999999999988897769753748434595763683319091796875
(3602879701896397, 36028797018963968)
36028797018963968

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stored_value.py

Converting a float to Decimal shows every digit of the value it holds. The float for 0.1 is a little more than 0.1, the one for 0.2 a little more than 0.2, and the one for 0.3 a little less than 0.3. As a fraction, as_integer_ratio() shows the stored 0.1 is 3602879701896397 divided by 2 to the power 55. When Python prints a float it chooses the shortest decimal that would be stored as the same float, which is why print(0.1) shows just 0.1 and the error stays hidden until a calculation pushes it into view.

IEEE 754 Floating-Point Converter Type 0.1 to see its 64 bits, the exact stored value and the rounding error.

A closer look: why the sum misses

Between two neighbouring floats there is no other float, so every result has to land on one of them. The sum of the stored 0.1 and 0.2 falls exactly halfway between the two floats nearest 0.3:

Halfway between two floats Python · halfway.py
import math
from fractions import Fraction

exact_sum = Fraction(0.1) + Fraction(0.2)  # the sum of the two stored values, exactly
below = Fraction(0.3)  # the float that 0.3 is stored as
above = Fraction(math.nextafter(0.3, 1))  # the next float up

print(below < exact_sum < above)
print(exact_sum - below == above - exact_sum)  # exactly halfway
print((0.3).hex(), (0.1 + 0.2).hex())  # the last digits: 3 is odd, 4 is even

Output

True
True
0x1.3333333333333p-2 0x1.3333333333334p-2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 halfway.py

The exact sum of the stored 0.1 and 0.2 lies halfway between the two floats nearest 0.3 and is rounded up to the upper one.The two floats nearest 0.3the exact sum of thestored 0.1 and 0.20.3 exactlya tie: it goes to the even float0.29999999999999998889…stored for 0.30.30000000000000004441…what 0.1 + 0.2 gives

Why 0.1 + 0.2 lands on the float above 0.3

Text description of the diagram

A short stretch of the number line shows two neighbouring floats, the closest ones to 0.3. No float lies between them.

  • The left one, 0.29999999999999998889…, is the float that 0.3 is stored as: it is the closest float to 0.3, which lies a little to its right.
  • The right one, 0.30000000000000004441…, is the next float up.
  • The exact sum of the stored values of 0.1 and 0.2 lies exactly halfway between the two. The addition has to round it to one of them, and a tie goes to the float whose last binary digit is even, which here is the right one.

So 0.1 + 0.2 gives the right-hand float, and 0.3 is stored as the left-hand one: two different floats, which is why 0.1 + 0.2 == 0.3 is False.

On a tie, the hardware rounds to the float whose last binary digit is even, IEEE 754’s default rule and the same “half to even” rule that round() uses. The hexadecimal forms printed above end in 3 and in 4, and the even one is the upper float. 0.3 written in your program is stored as the lower one, the float closest to 0.3. Two different floats, so == says False.

The limits of a float

How big and how precise Python · float_limits.py
import sys

print(sys.float_info.max)  # the largest float
print(sys.float_info.max * 10)  # past it: infinity, and no error
try:
    sys.float_info.max ** 2  # ** checks for overflow
except OverflowError:
    print("** raised OverflowError")
print(sys.float_info.epsilon)  # the gap between 1.0 and the next float up
print(2.0 ** 53 + 1 == 2.0 ** 53)  # above 2 ** 53 not every whole number fits
print(float(2 ** 53 + 1))

Output

1.7976931348623157e+308
inf
** raised OverflowError
2.220446049250313e-16
True
9007199254740992.0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 float_limits.py

  • The largest float is about 1.8 × 10³⁰⁸. Beyond it a multiplication gives inf (infinity) without an error, while ** raises OverflowError (the try and except lines catch it, as in the integers lesson): Python checks only some float operations for overflow.
  • sys.float_info.epsilon, about 2.2 × 10⁻¹⁶, is the gap between 1.0 and the next float. Every operation can be off by about half that much relative to its result, which is why floats are good for roughly 16 significant digits.
  • Whole numbers are exact only up to 2⁵³ (9,007,199,254,740,992). Above it the gaps between floats are wider than 1, so float(2 ** 53 + 1) comes back as 2⁵³. Python’s ints have no such limit, so keep counts and money in ints when you can.

Comparing floats: use math.isclose()

Because a computed float often carries a tiny rounding error, == is the wrong test for one. Ask instead whether two values are close enough:

Comparing with a tolerance Python · compare_floats.py
import math

total = 0.1 + 0.2
print(total == 0.3)
print(math.isclose(total, 0.3))  # within a relative tolerance of 1e-09

balance = 100.0 - 99.9 - 0.1  # should be zero
print(balance)
print(math.isclose(balance, 0.0))  # relative tolerance alone never accepts 0
print(math.isclose(balance, 0.0, abs_tol=1e-9))  # add an absolute tolerance

Output

False
True
-5.689893001203927e-15
False
True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compare_floats.py

math.isclose(a, b) accepts the two values when their difference is at most rel_tol times the larger of the two in size; the default rel_tol=1e-09 means about nine matching digits, whatever the size of the numbers. Near zero that rule fails: no number except 0 is close to 0 relative to its own size, so the balance, which should be zero but is -5.7 × 10⁻¹⁵, is rejected. Add an absolute tolerance, abs_tol, whenever a value may be zero or nearly so, and choose it from the problem: for an amount in rupees or dollars, such as this balance, 1e-9 is a ten-millionth of a paisa or a cent, far below anything a bill can contain.

math.isclose() follows the IEEE 754 rules for special values: infinity is close only to itself, and NaN (below) is close to nothing, not even to another NaN.

Adding up many floats

Each addition rounds its result, and the small errors can pile up:

A loop, sum() and math.fsum() Python · adding_up.py
import math

total = 0.0
for _ in range(10):
    total += 0.1  # ten additions, each one rounded
print(total)

print(sum([0.1] * 10))  # sum() keeps track of the rounding errors
print(math.fsum([0.1] * 10))

values = [1e16, 1e-16, 1.0]
print(sum(values), math.fsum(values))  # here only fsum() gets it right

Output

0.9999999999999999
1.0
1.0
1e+16 1.0000000000000002e+16

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 adding_up.py

The loop rounds after every step and ends just short of 1.0. sum() keeps track of the rounding errors as it goes (an algorithm called Neumaier summation) and gets exactly 1.0. math.fsum() goes further and tracks several partial sums, so no precision is lost along the way. The last line shows a case where only fsum() is right: the true total is just over 10000000000000001, and the nearest float to that is 1.0000000000000002e+16.

Version note

The more accurate sum() of floats is new in Python 3.12. Python 3.11 and older add the floats one by one, so sum([0.1] * 10) gives 0.9999999999999999 there, the same as the loop.

Rounding surprises

round(), formatting and int() Python · rounding_surprises.py
from decimal import Decimal

print(round(2.675, 2))  # 2.67, not 2.68
print(Decimal(2.675))  # the value really stored is a little below 2.675
print(f"{2.675:.2f}")  # formatting rounds the stored value too
print(round(0.125, 2))  # 0.125 is exact: a true tie, so half to even

percent = 0.57 * 100
print(percent)
print(int(percent))  # int() drops the fraction: 56
print(round(percent))  # round first: 57

Output

2.67
2.67499999999999982236431605997495353221893310546875
2.67
0.12
56.99999999999999
56
57

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rounding_surprises.py

round(2.675, 2) gives 2.67, not 2.68, because the float written as 2.675 is really a little below it, so there is no tie to round up. Formatting with :.2f rounds the same stored value and agrees. 0.125, on the other hand, is exactly ⅛, a real tie, and round() sends it to the even neighbour, 0.12.

A common mistake: int() after float arithmetic

0.57 * 100 is 56.99999999999999, and int() drops the fraction, so the result is 56 instead of 57. Percentages, prices in paise or cents and other “convert to a whole number” steps go wrong this way. Use round() instead, which also returns an int.

Rounding Calculator Round with exact decimal arithmetic and every rounding mode side by side.

Infinity, NaN and minus zero

The special float values Python · special_values.py
import math

inf = float("inf")
nan = float("nan")  # "not a number"
print(inf > 10 ** 300, -inf < 0)
print(inf - inf, inf * 0)  # no sensible answer: nan
print(nan == nan, nan != nan)  # nan is not equal even to itself
print(math.isnan(nan), math.isinf(inf), math.isfinite(1e308))
print(0.0 == -0.0, math.copysign(1, -0.0))  # equal, but the sign is kept

Output

True True
nan nan
False True
True True True
True -1.0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 special_values.py

  • float("inf") and -float("inf") are larger and smaller than every finite number.
  • nan (“not a number”) is the result of operations with no sensible answer, such as infinity minus infinity. Every comparison with it is False, even nan == nan; only != is True. Test for it with math.isnan(), and for infinity with math.isinf(), or for “an ordinary number” with math.isfinite().
  • -0.0 equals 0.0, but it keeps its sign, which math.copysign() can read.

When a float is the wrong type

The standard library has two other number types for work where binary rounding is not acceptable. A later lesson covers both in depth; this is when to reach for each:

Decimal and Fraction Python · exact_types.py
from decimal import Decimal
from fractions import Fraction

print(Decimal("0.1") + Decimal("0.2"))  # decimal arithmetic, from strings
print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))
print(Decimal(0.1))  # from a float: the float's error comes along

print(Fraction(1, 3) + Fraction(1, 6))  # exact ratios
print(Fraction(1, 3) * 3 == 1)

print(float.from_number(Fraction(1, 4)))  # new in 3.14: numbers only
try:
    float.from_number("3.5")  # float("3.5") would parse the text
except TypeError as error:
    print("TypeError:", error)

Output

0.3
True
0.1000000000000000055511151231257827021181583404541015625
1/2
True
0.25
TypeError: must be real number, not str

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exact_types.py

float, Decimal or Fraction?
Criterion floatdecimal.Decimalfractions.Fraction
Stores a binary fraction with 53 significant bitsdecimal digits: 28 significant digits by default, adjustablean exact numerator and denominator
Exact for halves, quarters, eighths … and whole numbers up to 2⁵³decimal amounts such as 0.1 and 19.99every ratio of two integers, such as 1/3
Arithmetic the processor's floating-point hardwaresoftwaresoftware, with numerators and denominators that can grow large
When to choose Measurements, science, graphics and most everyday calculationsMoney, and anything that must round the way people do with decimalsExact ratios, such as probabilities or recipe scaling

Build a Decimal from a string, Decimal("0.1"), or from an int. Decimal(0.1) copies the float exactly, error and all, as the third line of output shows.

Version note

New in Python 3.14: float.from_number() converts a number (an int, a Fraction, a Decimal …) to a float and raises TypeError for anything else, including text. float() still parses strings such as "3.5", so from_number() is the stricter choice when a value must already be a number.

Key takeaways

  • Floats are binary fractions with 53 significant bits; most decimal fractions, including 0.1, are stored as the nearest such value, so small errors are normal.
  • Never compare computed floats with ==. Use math.isclose(), with an abs_tol whenever values can be near zero.
  • sum() adds floats accurately since Python 3.12; math.fsum() is the most accurate.
  • round(2.675, 2) is 2.67 because of the stored value; use round(), not int(), when you turn a float result into a whole number.
  • NaN is not equal to itself: test it with math.isnan().
  • Use Decimal, built from strings, for money, and Fraction for exact ratios; keep floats for measurements and science.

Exercise

Exercise · Easy · Python

Compare floats with a tolerance

Write two functions in compare.py.

The first, almost_equal(a, b), returns True when the floats a and b are equal apart from rounding errors, and False otherwise. Use math.isclose() with its default relative tolerance (rel_tol=1e-09, about nine matching digits) and an absolute tolerance of ABS_TOL, which the starter code sets to 1e-12, so that results near zero also count as close.

The second, surprises(pairs), takes a list of (a, b) pairs and returns, in their original order, the pairs that == calls different but almost_equal() calls equal: the comparisons that == would get wrong.

  • almost_equal(0.1 + 0.2, 0.3) is True
  • almost_equal(1e-15, 0.0) is True
  • almost_equal(1e-9, 0.0) is False
  • almost_equal(float("nan"), float("nan")) is False
  • surprises([(0.1 + 0.2, 0.3), (1.0, 1.0), (2.0, 3.0)]) is [(0.30000000000000004, 0.3)]

The sample tests compare twelve pairs, among them infinities, NaN, 0.0 and -0.0, and values very close to zero.

Starter code · compare.py

import math

ABS_TOL = 1e-12  # how far apart two numbers near zero may be and still count as equal


def almost_equal(a, b):
    """Return True when a and b are equal within rel_tol=1e-09 or abs_tol=ABS_TOL."""
    # Replace this line with your code.
    return a == b


def surprises(pairs):
    """Return the pairs (a, b) that == calls different but almost_equal() calls equal, in order."""
    # Replace this line with your code.
    return []
The sample tests · test_compare.py
from compare import almost_equal, surprises

nan = float("nan")
inf = float("inf")


def test_rounding_errors():
    """treats numbers as equal when they differ only by a tiny fraction of their size"""
    assert almost_equal(0.1 + 0.2, 0.3) is True
    assert almost_equal(1.1 + 2.2, 3.3) is True
    assert almost_equal(1e20, 1e20 + 1e5) is True


def test_real_differences():
    """keeps numbers that really differ apart"""
    assert almost_equal(1.0, 1.001) is False
    assert almost_equal(100.0, 100.5) is False


def test_relative_tolerance():
    """allows a relative difference of 1e-09, and no more"""
    assert almost_equal(1.0, 1.0 + 5e-10) is True
    assert almost_equal(1.0, 1.0 + 2e-9) is False


def test_near_zero():
    """allows an absolute difference of 1e-12 near zero, and no more"""
    assert almost_equal(1e-15, 0.0) is True
    assert almost_equal(100.0 - 99.9 - 0.1, 0.0) is True
    assert almost_equal(2e-12, 0.0) is False
    assert almost_equal(1e-9, 0.0) is False


def test_signed_zero():
    """treats 0.0 and -0.0 as equal"""
    assert almost_equal(0.0, -0.0) is True


def test_special_values():
    """follows the IEEE 754 rules for infinity and NaN"""
    assert almost_equal(inf, inf) is True
    assert almost_equal(inf, -inf) is False
    assert almost_equal(nan, nan) is False
    assert almost_equal(nan, 1.0) is False


def test_surprises():
    """finds the pairs that == gets wrong, in their original order"""
    pairs = [
        (0.1 + 0.2, 0.3),
        (1.0, 1.0),
        (1e-15, 0.0),
        (1e-9, 0.0),
        (0.0, -0.0),
        (nan, nan),
        (inf, inf),
        (inf, -inf),
        (1e20, 1e20 + 1e5),
        (1.0, 1.001),
        (1.1 + 2.2, 3.3),
        (2.5, 2.5),
    ]
    assert surprises(pairs) == [(0.1 + 0.2, 0.3), (1e-15, 0.0), (1e20, 1e20 + 1e5), (1.1 + 2.2, 3.3)]
A hint

math.isclose(a, b, abs_tol=ABS_TOL) already uses rel_tol=1e-09, its default. For surprises(), start with an empty list, go through the pairs with for a, b in pairs:, and append((a, b)) when a != b and almost_equal(a, b) are both true.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 Why is 0.1 + 0.2 == 0.3 False in Python?

    Choose one answer.

    Show the answer to question 1

    Answer: 0.1, 0.2 and 0.3 are stored as the nearest binary fractions, and the sum lands on a different float from the one stored for 0.3

    Fractions with a factor other than 2 in the denominator, such as 1/10, never end in binary, so each literal is stored as the nearest float. The exact sum of the stored 0.1 and 0.2 lies halfway between two floats and is rounded up, while 0.3 is stored as the float below. Other languages that use the same binary floats agree.

  2. Question 2 of 5 What does this print?

    Read the code, then choose one answer.

    import math
    print(math.isclose(1e-10, 0.0))
    Show the answer to question 2

    Answer: False

    With only the default relative tolerance, a value is close to 0 only if it is 0: the allowed difference is rel_tol times the larger value, and that shrinks to nothing next to zero. Pass an absolute tolerance, for example abs_tol=1e-9, when values can be near zero.

  3. Question 3 of 5 Which of these values are stored exactly as floats?

    Choose every answer that is right.

    Show the answer to question 3

    Answer:

    • 0.125
    • 0.5
    • 0.75

    A fraction is exact in binary when its denominator, in lowest terms, is a power of 2: ½, ¾ and ⅛ qualify. 1/10, 1/5 and 1/3 have other factors in the denominator, so they are stored as the nearest float.

  4. Question 4 of 5 What does rounding_surprises.py print?

    What does this program print? Choose one answer.

    from decimal import Decimal
    
    print(round(2.675, 2))  # 2.67, not 2.68
    print(Decimal(2.675))  # the value really stored is a little below 2.675
    print(f"{2.675:.2f}")  # formatting rounds the stored value too
    print(round(0.125, 2))  # 0.125 is exact: a true tie, so half to even
    
    percent = 0.57 * 100
    print(percent)
    print(int(percent))  # int() drops the fraction: 56
    print(round(percent))  # round first: 57
    Show the answer to question 4

    Answer: it prints

    2.67
    2.67499999999999982236431605997495353221893310546875
    2.67
    0.12
    56.99999999999999
    56
    57

    The float written as 2.675 is a little below 2.675, so both round() and :.2f give 2.67. 0.125 is exact, a real tie, and goes to the even neighbour, 0.12. 0.57 * 100 is just under 57, so int() gives 56 and round() gives 57.

  5. Question 5 of 5 A shop's program adds up prices such as 19.99 and must match the receipt to the last paisa or cent. Which choice fits?

    Choose one answer.

    Show the answer to question 5

    Answer: Decimal values built from strings, such as Decimal("19.99")

    Decimal does decimal arithmetic, so 19.99 is exact when it comes from the string "19.99". Built from a float, a Decimal or a Fraction copies the float's binary error exactly. Rounding floats after every step still starts from inexact values. (Counting in whole paise or cents with ints works too.)

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.