Calculate Money with Decimal
Represent decimal money from text and round a final amount to cents with an explicit rule.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Calculate Money with Decimal
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Money calculations need more than arithmetic that looks plausible for one price. They need a representation of the original amount and an explicit rule for rounding the final result. Python's float type is excellent for many scientific and general computations, but its finite binary representation cannot express every decimal fraction exactly. Python's Decimal can represent values built from decimal text as intended, yet even Decimal does not automatically decide when to round to cents. We will reproduce a familiar float surprise, inspect why passing a float into Decimal does not undo that approximation, and then build a small discount function that accepts text, validates the percent, rounds the final result to two decimal places, and returns a stable string.
- 00:51
The companion website task includes a half-cent boundary and a full-discount case, so a solution that only looks good for 12.50 will not be enough. The first comparison is false because those float values are binary approximations. This is expected behavior for finite binary floating-point, not a random failure. The second line is also false: Decimal(0.1) starts from the already approximated float, so it preserves that approximation rather than recovering the original text "0.1". The third line is true because each Decimal was constructed directly from decimal text.
- 01:31
Our discount function receives text from its boundary, so it should pass that text directly to Decimal before any float conversion. This solves representation, but not output precision. Multiplying a price by a percentage can produce three or more decimal places. A financial application must say whether and when it rounds. Different systems can use different legal or accounting rules. For this lesson, we will explicitly use two decimal places and ROUND_HALF_UP, then test that choice at a tie. The first line is exactly 1.005, which is what the text said. Quantize with a target of 0.01 requests two decimal places, and ROUND_HALF_UP sends this half-cent value to 1.01.
- 02:20
The last multiplication illustrates another issue: Decimal preserves significance in its exponent, so a normal discount can produce 10.0000. That is a valid numeric value but not the desired two-place display string. Quantizing the final result solves the output contract. We should keep intermediate arithmetic unrounded unless a specification requires earlier rounding, because premature rounding can change a total built from several operations. We also need to validate that the percentage lies between zero and one hundred. Decimal faithfully represents a negative percent too; it cannot decide that a discount of minus five percent is disallowed by our exercise.
- 03:04
Representation, domain validation, and rounding are three separate responsibilities. Both inputs remain strings until Decimal parses them, so no float approximation enters the calculation. We reject an out-of-range discount before doing the arithmetic. The formula keeps its decimal precision until the final amount, where quantize applies the stated cent rule. The result is converted to a string with two decimal places, including a trailing zero when needed. That stable display shape matters for a price label and for comparing expected output in a practice task. Notice that str of the quantized value is doing display work; the Decimal should remain a Decimal if another calculation needs to follow.
- 03:51
Repeatedly converting numbers to display strings and back makes later arithmetic harder to audit. A larger program could return a Decimal from its calculation layer and format it at its presentation layer. This compact task returns a string because its contract is a finished price label. This example deliberately does not specify currency conversion, taxes, or negative prices; those would need their own domain rules. It also does not claim that ROUND_HALF_UP is universally correct. Some systems use round-half-even or legally prescribed rules. The important skill is to choose and state a policy.
- 04:32
On the companion site, the starter function already uses Decimal text inputs but returns the unrounded amount. Add the range check and final quantize operation, then run the checker. Twelve-fifty with twenty percent off gives ten dollars shown as 10.00, not 10 or 10.0000. A ten-cent price with fifty percent off gives five cents. The half-cent example verifies the chosen tie rule. A full discount produces 0.00, preserving the two-place money format. The final loop confirms that a negative percentage and a percentage above one hundred are rejected. Together, these cases detect representation mistakes, missing rounding, inconsistent formatting, and missing validation.
- 05:17
There is a subtle reason to state where rounding happens. If several discounted items are rounded individually and then summed, the final total can differ from summing their exact intermediate amounts and rounding once. Neither order is automatically universal; a receipt or tax rule must say which one applies. This lesson chooses one final quantize for one price. It does not pretend to define a complete checkout system. In an actual billing system, you would also specify currency, tax order, and any jurisdictional rounding requirements before relying on a function like this. For this educational contract, the logic is intentionally narrow and testable.
- 06:00
Try a price of 19.99 with ten percent off on the website and predict the result before running it. Then change the rounding mode in a separate experiment and observe which boundary outputs change. That turns a vague concern about floating-point into a precise design decision.
Understand the concept
Binary floating-point cannot represent many familiar decimal fractions exactly. For example, 0.1 + 0.2 is not exactly 0.3 as a float comparison. That does not make float defective; it follows a binary representation with finite precision. Financial amounts often need an explicit decimal representation and rounding rule. Python's Decimal type can represent values constructed from decimal text exactly.
Construct Decimal('0.10') from text, not Decimal(0.10). The latter captures the existing binary float approximation rather than the intended two-digit decimal amount. Integer values are exact too, so Decimal(100) is safe for percent conversion. Do not assume Decimal alone rounds automatically to cents: after multiplying, a value may have more than two decimal places.
Use quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) when the lesson's stated policy is round half up to two decimal places. Other domains use other rules, such as ROUND_HALF_EVEN; choose the rule required by the system rather than treating one as universally correct. Keep intermediate arithmetic unrounded unless the contract explicitly requires an earlier rounding step.
The checked function discounted_total(price_text, percent_text) parses both values as Decimal, validates a percent between zero and one hundred, calculates the discount, rounds once to cents, and returns a two-decimal-place string. A price of 1.005 with a zero-percent discount deliberately tests a half-cent boundary. The task also tests a full discount and small amounts.
- Construct Decimal from decimal text
- Decimal(float) preserves a float approximation
- quantize selects the number of decimal places
- A rounding rule is a domain decision
See it step by step
Read the code, predict the output, then compare it with the result.
01. Compare binary float and exact decimal text
from decimal import Decimal
print(0.1 + 0.2 == 0.3)
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3'))False
TrueDecimal constructed from text expresses these base-ten fractions exactly; float uses binary approximations.
02. Round a half cent explicitly
from decimal import Decimal, ROUND_HALF_UP
amount = Decimal('1.005')
print(amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))1.01The quantize target sets two decimal places and ROUND_HALF_UP states the tie rule.
03. Keep two digits after a discount
from decimal import Decimal, ROUND_HALF_UP
price = Decimal('12.50')
rate = Decimal('20') / Decimal('100')
final = (price * (1 - rate)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
print(str(final))10.00Rounding once after the calculation yields a string with exactly two decimal places.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Start with the original decimal text
A float stores a finite binary approximation of many decimal fractions. The familiar comparison 0.1 + 0.2 == 0.3 is false because the represented values are not exactly those base-ten fractions. Decimal can preserve the decimal values when it is constructed from their original text. Passing a float into Decimal imports the already-approximated binary value instead.
This is not a claim that float is unusable. It is a reminder to choose a representation and rounding policy for the problem at hand. For the money exercise, the public inputs are decimal strings, so parsing them directly into Decimal is natural. Read both lines of the example and predict which equality test will be true.
from decimal import Decimal
print(0.1 + 0.2 == 0.3)
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3'))
print(Decimal('0.1'))False
True
0.1- Binary float approximates the decimal fractions.
- Decimal from text represents these simple decimal inputs exactly.
- The original text is kept out of a float conversion step.
Choose cent precision and a rounding rule
Decimal arithmetic does not automatically know when an application wants cents. An amount such as 1.005 has three decimal places. Quantize with Decimal('0.01') sets the result to two places. The ROUND_HALF_UP option tells the operation what to do at a half-cent tie. Another domain might require a different rounding method, so the rule belongs in the contract and test cases.
The discount computation should keep its intermediate precision and quantize the final amount once, unless a business specification says otherwise. Early rounding can produce a different total after several operations. The example shows a half-cent input and a normal twenty-percent discount, both rendered with exactly two digits after the decimal point.
from decimal import Decimal, ROUND_HALF_UP
cent = Decimal('0.01')
print(Decimal('1.005').quantize(cent, rounding=ROUND_HALF_UP))
price = Decimal('12.50')
amount = price * (Decimal(100) - Decimal(20)) / Decimal(100)
print(amount.quantize(cent, rounding=ROUND_HALF_UP))1.01
10.00- The cent quantizer fixes two decimal places.
- ROUND_HALF_UP turns 1.005 into 1.01.
- The discount is calculated before rounding its final value.
Validate the discount and return a stable string
The finished function accepts decimal text for both price and percentage. It rejects a percentage below zero or above one hundred. It then computes the discounted amount and rounds only the final result. Returning str of the quantized Decimal preserves two decimal places, including trailing zeros that are meaningful for displayed money values.
The checked task tries an ordinary discount, a small amount, a full discount, and a half-cent case. It also probes percentages outside the allowed interval. A version that returns the unquantized amount may pass one ordinary example but will fail formatting and rounding cases. A version that builds Decimal from floats can inherit binary approximation before the decimal arithmetic even starts.
from decimal import Decimal, ROUND_HALF_UP
def discounted_total(price_text, percent_text):
price = Decimal(price_text)
percent = Decimal(percent_text)
if not 0 <= percent <= 100:
raise ValueError('percent must be from 0 to 100')
amount = price * (Decimal(100) - percent) / Decimal(100)
return str(amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
print(discounted_total('12.50', '20'))
print(discounted_total('1.005', '0'))10.00
1.01- Parse the original strings as Decimal values.
- Validate the percent before calculating.
- Quantize once and return two-decimal-place text.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
After computing amount, call amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP). Check the percent before computing. Return str(rounded).
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Constructing Decimal from an existing float
price = Decimal(0.1)price = Decimal('0.1')What happens: The object retains the binary float's approximation instead of an exact tenth.
Parse the original decimal text before binary floating-point changes it.
Returning an unrounded amount
return str(price * (1 - rate))return str(amount.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))What happens: Results may have more than two fractional digits, such as 1.005.
State a cent precision and explicit rounding rule at the output boundary.