Warm-up · Activity 1 of 7
// A4.4 · ~35 min · Advanced
Floating point, decimal and fractions
After this lesson you can explain why 0.1 + 0.2 != 0.3, compare floats with math.isclose, compute money exactly with Decimal from strings and quantize, and use Fraction for exact ratios.
You will be able to
- Explain representation error and compare floats with math.isclose, including near zero
- Compute exactly with decimal.Decimal from strings, round with quantize and set precision in a context
- Use fractions.Fraction for exact rational arithmetic, and choose between float, Decimal and Fraction
Predict · Activity 2 of 7
Predict before you read on: what does this print?
print(0.1 + 0.2 == 0.3, 0.1 + 0.2)Practice · Activity 3 of 7
leftover is 5.551115123125783e-17, rounding noise. Fill in the keyword so that the check treats it as zero.
result = math.isclose(leftover, 0.0, ____=1e-12)result = math.isclose(leftover, 0.0, =1e-12)Practice · Activity 4 of 7
What does this print?
from decimal import Decimal print(Decimal("0.1") * 3 == Decimal("0.3"), Decimal(0.1) * 3 == Decimal("0.3"))Practice · Activity 5 of 7
Match each expression to its result.
Brain teaser · Activity 6 of 7
Brain teaser. What does this print?
print(round(2.675, 2), round(0.5), round(1.5), round(2.5))Apply · Activity 7 of 7
Mini-task. Write split(total, people): total is a string such as "10.00", and the result is a list of Decimal shares in cents that add up to exactly the total. Round each share down to cents with quantize, then give the leftover cents, one each, to the first people. split("10.00", 3) gives 3.34, 3.33 and 3.33. Check the sum with ==, not with isclose.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
One invoice, three number types
Three lines of an invoice are added up three ways. With floats, the net total is already slightly off. With Decimal built from strings, it is exact, and the 19 % VAT is rounded to cents once, on purpose; the two rounding modes give different cents for an exact half. Finally the total is split three ways with Fraction, which keeps the third exact until you choose to round it.
main.py
import math
from decimal import ROUND_HALF_EVEN, ROUND_HALF_UP, Decimal
from fractions import Fraction
CENT = Decimal("0.01")
VAT_RATE = Decimal("0.19")
lines = [("sugar", "0.10", 2), ("milk", "0.70", 1), ("coffee", "2.20", 3)]
# 1. Floats: every price is already a little off, and the errors add up.
net_float = sum(float(price) * quantity for _, price, quantity in lines)
print("float net:", net_float)
print("== 7.5:", net_float == 7.5, "| isclose:", math.isclose(net_float, 7.5))
# 2. Decimal from strings: exact, and rounded once, on purpose.
net = sum((Decimal(price) * quantity for _, price, quantity in lines), Decimal("0"))
vat = net * VAT_RATE
print("net:", net, "| exact VAT:", vat)
print("VAT half up: ", vat.quantize(CENT, rounding=ROUND_HALF_UP))
print("VAT half even:", vat.quantize(CENT, rounding=ROUND_HALF_EVEN))
total = net + vat.quantize(CENT, rounding=ROUND_HALF_UP)
print("total:", total)
# 3. Fraction: an exact third, where Decimal and float must round.
share = Fraction(total) / 3
print("a third:", share, "~", round(float(share), 4))
Run it with
python main.pyOutput
float net: 7.500000000000001
== 7.5: False | isclose: True
net: 7.50 | exact VAT: 1.4250
VAT half up: 1.43
VAT half even: 1.42
total: 8.93
a third: 893/300 ~ 2.9767- The float net is off in the 16th digit, enough to make == fail; isclose still accepts it.
- The Decimal net keeps two places, 7.50, because its inputs had two.
- 1.4250 is an exact half between 1.42 and 1.43: HALF_UP goes up, HALF_EVEN goes to the even 2.
- Fraction(total) converts the Decimal exactly, and 893/300 stays exact until round.
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercises
Exercise 1 of 3
An exact invoice total
invoice_total(lines, vat_rate) gets (price, quantity) pairs with prices as strings, and a rate such as "0.19". It must return net plus VAT as a Decimal, with the VAT rounded half up to cents, so 7.50 net at 19 % gives 8.93. The starter uses floats and round. Rewrite it with Decimal built from the strings and quantize.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
Decimal(price) * quantity for each line; start sum at Decimal("0") so the result stays a Decimal.
Hint 2
VAT is (net * Decimal(vat_rate)).quantize(CENT, rounding=ROUND_HALF_UP).
Hint 3
Return net + vat. Never pass a float to Decimal here: its error would come along exactly.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
from decimal import ROUND_HALF_UP, Decimal
CENT = Decimal("0.01")
def invoice_total(lines: list[tuple[str, int]], vat_rate: str) -> Decimal:
"""Net total of (price, quantity) lines plus VAT, the VAT rounded half up to cents."""
net = sum((Decimal(price) * quantity for price, quantity in lines), Decimal("0"))
vat = (net * Decimal(vat_rate)).quantize(CENT, rounding=ROUND_HALF_UP)
return net + vat
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from decimal import ROUND_HALF_UP, Decimal
CENT = Decimal("0.01")
def invoice_total(lines: list[tuple[str, int]], vat_rate: str) -> Decimal:
"""Net total of (price, quantity) lines plus VAT, the VAT rounded half up to cents."""
net = sum(float(price) * quantity for price, quantity in lines)
return Decimal(round(net * (1 + float(vat_rate)), 2))
test_main.py
from decimal import Decimal
from main import invoice_total
def test_half_up():
"""7.50 net at 19 % gives VAT 1.4250, rounded up to 1.43: 8.93"""
got = invoice_total([("0.10", 2), ("0.70", 1), ("2.20", 3)], "0.19")
assert got == Decimal("8.93"), f"got {got!r}, expected Decimal('8.93')"
def test_returns_decimal_cents():
"""The result is a Decimal with two places"""
got = invoice_total([("19.99", 3)], "0.07")
assert isinstance(got, Decimal), f"got a {type(got).__name__}, expected a Decimal"
assert str(got) == "64.17", f"got {str(got)!r}, expected '64.17' (59.97 + 4.20)"
def test_empty_invoice():
"""No lines: 0.00"""
got = invoice_total([], "0.19")
assert str(got) == "0.00", f"got {str(got)!r}, expected '0.00'"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 3
Readings that match
readings_match(a, b) should say whether two float readings are equal up to floating-point error. The starter uses ==, so 0.1 + 0.2 and 0.3 do not match. Use math.isclose with its default relative tolerance and abs_tol=1e-12, so that tiny leftovers near zero match 0.0, while 1e-6 against 0.0 still does not.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
math.isclose(a, b) already handles 0.1 + 0.2 against 0.3 with its default rel_tol of 1e-09.
Hint 2
Near zero a relative tolerance is useless: rel_tol * abs(0.0) is 0.0. That is what abs_tol is for.
Hint 3
return math.isclose(a, b, abs_tol=1e-12)
Show a solution
One way to solve it. Yours can look different and still pass the checks.
import math
def readings_match(a: float, b: float) -> bool:
"""True when two sensor readings are equal up to floating-point error."""
return math.isclose(a, b, abs_tol=1e-12)
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
import math
def readings_match(a: float, b: float) -> bool:
"""True when two sensor readings are equal up to floating-point error."""
return a == b
test_main.py
from main import readings_match
def test_representation_error():
"""0.1 + 0.2 matches 0.3"""
assert readings_match(0.1 + 0.2, 0.3), "0.1 + 0.2 and 0.3 differ only by representation error"
def test_near_zero():
"""The leftover of 0.1 + 0.2 - 0.3 matches 0.0"""
leftover = 0.1 + 0.2 - 0.3
assert readings_match(leftover, 0.0), f"{leftover!r} should match 0.0: near zero, only abs_tol helps"
def test_real_difference():
"""1.0 and 1.0001 do not match"""
assert not readings_match(1.0, 1.0001), "1.0 and 1.0001 differ by far more than rounding error"
def test_small_but_real():
"""1e-6 does not match 0.0"""
assert not readings_match(1e-6, 0.0), "1e-6 is a real value, not rounding noise: keep abs_tol tiny"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 3 of 3
An exact mean
exact_mean(values) gets numbers as strings, such as "1/3" or "0.25", and must return their mean as an exact Fraction: the mean of three thirds is exactly 1/3, and of 0.1, 0.2 and 0.3 exactly 1/5. The starter goes through float and loses that. Parse each string with Fraction and never convert to float.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
Fraction("1/3") and Fraction("0.25") are exact; float(...) is where the error comes in.
Hint 2
Start the sum at Fraction(0): sum((Fraction(v) for v in values), Fraction(0)).
Hint 3
A Fraction divided by an int stays a Fraction, so divide by len(values) at the end.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
from fractions import Fraction
def exact_mean(values: list[str]) -> Fraction:
"""The exact mean of values such as "1/3" or "0.25"."""
return sum((Fraction(v) for v in values), Fraction(0)) / len(values)
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from fractions import Fraction
def exact_mean(values: list[str]) -> Fraction:
"""The exact mean of values such as "1/3" or "0.25"."""
return Fraction(sum(float(Fraction(v)) for v in values) / len(values))
test_main.py
from fractions import Fraction
from main import exact_mean
def test_thirds():
"""1/3, 1/3 and 1/3 average to exactly 1/3"""
got = exact_mean(["1/3", "1/3", "1/3"])
assert got == Fraction(1, 3), f"got {got!r}, expected Fraction(1, 3)"
def test_decimal_strings():
"""0.1, 0.2 and 0.3 average to exactly 1/5"""
got = exact_mean(["0.1", "0.2", "0.3"])
assert got == Fraction(1, 5), f"got {got!r}, expected Fraction(1, 5): parse the strings, not floats"
def test_mixed():
"""1/2 and 0.25 average to 3/8"""
got = exact_mean(["1/2", "0.25"])
assert got == Fraction(3, 8), f"got {got!r}, expected Fraction(3, 8)"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyCommon mistakes
Adding a float to a Decimal
from decimal import Decimal
price = Decimal("19.99")
print(price + 0.01)
What Python prints
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'Why, and the fix
Decimal refuses floats in arithmetic, because the float’s binary error would leak into an exact value. Write the constant as a Decimal too: price + Decimal("0.01"). Adding an int works, because ints are exact.
A decimal comma in the string
from decimal import Decimal
price = Decimal("12,50")
What Python prints
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]Why, and the fix
Decimal parses Python’s number syntax, with a decimal point. Convert German input first, for example text.replace(".", "").replace(",", ".") for "1.234,50". Validate user input and catch decimal.InvalidOperation.
Passing a string to quantize
from decimal import Decimal
print(Decimal("1.005").quantize("0.01"))
What Python prints
TypeError: conversion from str to Decimal is not supportedWhy, and the fix
quantize takes the exponent as a Decimal: amount.quantize(Decimal("0.01")). Define it once as a constant, such as CENT = Decimal("0.01"), and pass rounding= explicitly so the rounding rule is visible in the code.
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.