Mira is preparing a practice receipt for a craft stall. A bookmark costs 1.25 units and a notebook costs 3.45 units. Both receive a 10% discount. She rounds the discounted prices on separate receipt lines, then adds them. Her total is 4.24.
Her friend adds the prices, applies the same discount and rounds once at the end. The friend's total is 4.23. Nobody mistyped a price. They rounded at different stages.
Before changing the code, Mira needs to decide which calculation her receipt should use. We will follow her two items through both methods, using Python's Decimal type to make the numbers and rounding choices visible. The prices and receipt rule are invented for this example; “units” do not stand for a particular currency.
You should be able to read a Python function, a list and a loop. You can follow the calculations without running anything. To try the code, use a fresh Python 3 session; decimal is part of the standard library.
Decide what one receipt line means
For Mira's receipt, choose this rule:
- Read each original price from decimal text.
- Multiply it by
0.90, because 90% remains after the discount. - Round that discounted line to two decimal places. Choose the nearest value; if it is exactly halfway, round away from zero.
- Add the rounded lines.
“Round to two places” alone would leave two questions unanswered: what happens at a tie, and when does rounding happen? Keep both decisions beside the code. They explain the receipt more clearly than “that is what Python returned.”
Start with the price written on the label
Python's usual float uses binary floating-point arithmetic. Many decimal fractions can only be approximated in that representation. Displaying two decimal places does not undo an approximation introduced earlier. Python's floating-point tutorial
For this calculation, start Decimal from strings:
from decimal import Decimal
price = Decimal("1.25")
remaining = Decimal("0.90")
print(price * remaining)
The result is 1.1250: the bookmark's discounted price before rounding. The quotation marks matter. Decimal("0.1") reads decimal text, while Decimal(0.1) converts an already-created float. Decimal construction
Mira keeps the sample prices as text until this conversion. Her calculation can then move directly from decimal text to Decimal values without passing through float.
Tell Python how to handle a halfway value
To round a Decimal, use quantize(). Giving it Decimal("0.01") requests two places after the decimal point. ROUND_HALF_UP supplies our chosen tie rule: nearest rounding, with exact halfway cases going away from zero. Decimal quantize and rounding
from decimal import Decimal, ROUND_HALF_UP
STEP = Decimal("0.01")
def round_line(amount):
return amount.quantize(STEP, rounding=ROUND_HALF_UP)
assert round_line(Decimal("1.125")) == Decimal("1.13")
assert round_line(Decimal("1.124")) == Decimal("1.12")
assert round_line(Decimal("-1.125")) == Decimal("-1.13")
An assert checks a claim. These checks finish silently when the values agree; a failed check raises AssertionError. The first two distinguish a halfway value from a nearby value. The negative example shows why “up” does not mean “toward positive infinity.”
A different rule, ROUND_HALF_EVEN, makes 1.125 become 1.12, choosing an even last retained digit at the tie. That is a different requirement, rather than a correction to Mira's arithmetic. Python's rounding modes
Follow both items to the total
Continue with the imports and round_line() above:
prices = [Decimal("1.25"), Decimal("3.45")]
remaining = Decimal("0.90")
raw_lines = [price * remaining for price in prices]
rounded_lines = [round_line(value) for value in raw_lines]
total = sum(rounded_lines, Decimal("0.00"))
for value in rounded_lines:
print(f"Line: {value:.2f}")
print(f"Total: {total:.2f}")
The two list expressions process one value at a time: first calculate every discounted price, then round each result. sum() adds the rounded values, starting from a Decimal zero. The :.2f inside each printed string displays two decimal places.
The output is:
Line: 1.13
Line: 3.11
Total: 4.24
Here are the steps behind those three lines:
| Item | Original price | After discount, before rounding | Rounded line |
|---|---|---|---|
| Bookmark | 1.25 | 1.1250 | 1.13 |
| Notebook | 3.45 | 3.1050 | 3.11 |
Now reproduce the friend's method using the same unrounded values:
round_once = round_line(sum(raw_lines, Decimal("0.00")))
print(f"Round only once: {round_once:.2f}")
This prints Round only once: 4.23. The friend adds 1.1250 + 3.1050, obtaining 4.2300, before rounding. Mira adds 1.13 + 3.11. Their one-hundredth difference comes from the rounding stage.
Both paths use the same halfway rule. The point at which it is applied changes the total.
Decimal lets Mira inspect those steps. It cannot choose the receipt policy for her. For a real receipt, establish the required rule before implementing it.
Your turn
Replace the two prices with 2.35 and 4.15. Keep the 10% discount and the same line-by-line halfway rule. Before running Python, work out the unrounded prices, the two rounded lines and their total. Then calculate the result of rounding only the unrounded sum.
Show the worked answer
The unrounded prices are 2.1150 and 3.7350. Their rounded lines are 2.12 and 3.74, so Mira's receipt total is 5.86. Adding before rounding gives 5.8500, which becomes 5.85.
exercise_raw = [Decimal("2.35") * remaining,
Decimal("4.15") * remaining]
exercise_lines = [round_line(x) for x in exercise_raw]
assert exercise_lines == [Decimal("2.12"), Decimal("3.74")]
assert sum(exercise_lines, Decimal("0.00")) == Decimal("5.86")
assert round_line(sum(exercise_raw, Decimal("0.00"))) == Decimal("5.85")
What to remember
The complete example script (the complete code is below) also sets arithmetic precision to 28 significant digits inside a local decimal context. That limit concerns arithmetic, while quantize() chooses our final decimal places. A local context restores the previous settings when its block ends. Decimal contexts
Our small products fit within that precision. A calculation such as one divided by three still needs a finite precision limit, even with Decimal. To explain a result, keep the original values, the arithmetic and the rounding stage visible. Mira can account for every hundredth on her receipt because she can follow each one of those decisions.
Complete optional Python example
"""Synthetic Decimal receipt lesson. Run with Python 3, no external packages.
Chosen teaching rule only: round every discounted line to 0.01 using
ROUND_HALF_UP, then sum. Not legal, tax, or accounting advice.
"""
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP, localcontext
STEP = Decimal("0.01")
REMAINING = Decimal("0.90")
def round_line(amount):
return amount.quantize(STEP, rounding=ROUND_HALF_UP)
def receipt(price_texts):
# Inputs here are small, finite, synthetic decimal strings. This is not a
# production input-validation or currency-handling implementation.
with localcontext() as context:
context.prec = 28
prices = [Decimal(text) for text in price_texts]
raw = [price * REMAINING for price in prices]
rounded = [round_line(value) for value in raw]
total = sum(rounded, Decimal("0.00"))
round_once = round_line(sum(raw, Decimal("0.00")))
return raw, rounded, total, round_once
def main():
assert Decimal("0.1") != Decimal(0.1)
assert round_line(Decimal("1.125")) == Decimal("1.13")
assert round_line(Decimal("1.124")) == Decimal("1.12")
assert round_line(Decimal("-1.125")) == Decimal("-1.13")
assert Decimal("1.125").quantize(STEP, rounding=ROUND_HALF_EVEN) == Decimal("1.12")
raw, rounded, total, round_once = receipt(["1.25", "3.45"])
assert raw == [Decimal("1.1250"), Decimal("3.1050")]
assert rounded == [Decimal("1.13"), Decimal("3.11")]
assert total == Decimal("4.24")
assert round_once == Decimal("4.23")
assert total - round_once == Decimal("0.01")
assert [str(value) for value in rounded] == ["1.13", "3.11"]
for value in rounded:
print(f"Line: {value:.2f}")
print(f"Total: {total:.2f}")
print(f"Round only once: {round_once:.2f}")
raw, rounded, total, round_once = receipt(["2.35", "4.15"])
assert raw == [Decimal("2.1150"), Decimal("3.7350")]
assert rounded == [Decimal("2.12"), Decimal("3.74")]
assert total == Decimal("5.86")
assert round_once == Decimal("5.85")
print(f"Exercise: line total {total:.2f}; round once {round_once:.2f}")
print("All Decimal assertions passed.")
if __name__ == "__main__":
main()