Build a till program for a small shop that reads items, applies multi-buy and order discounts, adds VAT, takes payment and prints an aligned receipt.
About 75 minBeginner6 exercisesFree
You will be able to
Turn a short requirements brief into a plan of inputs, calculations and outputs
Build a program in milestones, keeping every earlier requirement working
Read a variable number of input lines with a sentinel loop and accumulate totals
Apply discount and tax rules in the right order and round money correctly
Print an aligned receipt with f-string widths, and reject bad input lines without crashing
The brief
Cedar Corner Market is a family-run grocery shop. Its old till broke, and until the new one arrives the owner, Huda, wants a small program that prints receipts. She writes down what she needs:
As the person at the till, I want to type in each item the customer buys and get a clear, correct receipt, so that customers trust the total and I can give the right change.
Input. One item per line in the form name, quantity, unit price, for example Milk 1L, 3, 6.20. The word done (in any capitals) ends the list. After that, one more line: the cash the customer hands over.
Every item line shows the name, quantity, unit price and line total, in aligned columns.
Buying 3 or more of the same item takes 10% off that line (a "multi-buy").
If the subtotal after multi-buys is 200 SAR or more, a further 5% comes off the order.
VAT of 15% is added to the amount after all discounts.
Each discount and the VAT are rounded to 2 decimal places when they are calculated.
The receipt shows what was paid and the change, or how much is still owed.
A badly typed line never crashes the till: it is skipped and listed at the bottom of the receipt.
You'll build this in six milestones. Each one is a complete, working program; the next milestone starts from it.
Design: inputs, calculations, outputs
Before writing code, sort the brief into the three parts every program has:
Part
What it is here
Input
item lines until done, then the cash paid
Calculation
line total = quantity × price; multi-buy discount; subtotal; order discount; VAT; total; change
Output
header, one row per item, totals block, payment block, skipped lines
Three decisions shape the code:
A sentinel loop. You don't know how many items a customer has, so the program reads lines in a while loop until it meets done. The word that stops the loop is called a sentinel.
Accumulators.count and subtotal start at 0 and grow by one item each time round the loop.
Constants for the rules. The 15% VAT, the 10% multi-buy and the 200 SAR threshold are business rules that will change one day. Naming them once at the top (VAT_RATE = 0.15) means one edit, not a hunt through the code.
Here is the core loop on its own, with fixed lines so you can watch it work:
A sentinel loop that splits each line into fields
subtotal =0
line =input()while line.strip().lower()!="done":
name, qty_text, price_text = line.split(",")
line_total =int(qty_text)*float(price_text)print(name.strip(),"->", line_total)
subtotal += line_total
line =input()print("Subtotal:", subtotal)
Input given to the program: "Tea, 2, 11.50", "Rice 5kg, 1, 39.00", "DONE"
Try it: Add a line "Sugar 1kg, 4, 5.25" before DONE in your head and work out the new subtotal, then check it by editing the input when you run it.
Notice that int(" 2") and float(" 11.50") both accept the spaces left over after split(",") — but the name keeps its leading space, so it needs strip(). And line.strip().lower() lets the till operator type done, Done or DONE .
Milestone · ●●difficulty 2 of 5 · about 10 min
Milestone 1: read and list the items
Start the till. Read item lines with input() (no prompt text) until a line that says done in any capitals, ignoring spaces around it. For each item:
split the line at the commas into name, quantity and unit price;
strip the spaces from the name, convert the quantity with int() and the price with float();
print <name> x<qty> @ <price> = <line total> with both amounts to 2 decimal places;
add 1 to a variable named count.
After the loop print Items: <count>. For the input
Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.
Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type
Output
Run your code to see its output, or Check to test it against the requirements.
The solution becomes available after you pass, or after all three hints and two checks.
Milestone 2: totals and VAT
The list works; now the money. Keep a running subtotal beside count, then work out the VAT once after the loop. Because the VAT is money the shop pays to the tax authority, round it to 2 decimal places when you calculate it, and add the rounded value to the subtotal. That way the printed VAT and the printed total always agree.
Milestone · ●●difficulty 2 of 5 · about 8 min
Milestone 2: subtotal, VAT and total
Extend the till so that it adds up the order.
Add a constant VAT_RATE = 0.15 at the top.
Keep a variable subtotal (the sum of all line totals).
After Items:, calculate vat = round(subtotal * VAT_RATE, 2) and total = subtotal + vat, then print three lines, amounts to 2 decimals:
Subtotal: 107.60
VAT 15%: 16.14
TOTAL: 123.74
(Those are the figures for the three sample items.) The item lines and the Items: line stay exactly as in milestone 1.
Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.
Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type
Output
Run your code to see its output, or Check to test it against the requirements.
The solution becomes available after you pass, or after all three hints and two checks.
Milestone 3: aligned columns
Huda tapes a sample receipt to the wall: columns must line up so a customer can run a finger down the totals. An f-string format specification sets a width and alignment: {name:<18} pads to 18 characters, left-aligned; {price:>9.2f} right-aligns in 9 characters with 2 decimals. Adding the widths 18 + 4 + 9 + 10 gives a 41-character receipt. Labels in the totals block take the first 31 characters (18 + 4 + 9), so their amounts sit under the Total column.
A long name like Saffron threads premium grade would push its row out of line, so you cut names to their first 17 characters with a slice, name[:17], leaving at least one space before the quantity.
Header row: Item left in 18, Qty right in 4, Price right in 9, Total right in 10. The rules are 41 dashes.
Item rows: the name cut to 17 characters (name[:17]) and left-aligned in 18, then quantity (width 4), price (>9.2f) and line total (>10.2f).
Totals: the label left-aligned in 31, the amount >10.2f.
The calculations do not change.
Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.
Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type
Output
Run your code to see its output, or Check to test it against the requirements.
The solution becomes available after you pass, or after all three hints and two checks.
Milestone 4: discounts in the right order
Discounts are where real tills go wrong, because the order of the rules changes the answer. The brief fixes it:
Multi-buy comes first, per line: 10% off a line of 3 or more.
The subtotal is the sum of the lines after multi-buy.
The order discount (5% from 200 SAR) is worked out on that subtotal.
VAT is charged on what the customer actually pays for: subtotal minus order discount.
"200 SAR or more" means >=, not >. A subtotal of exactly 200.00 gets the discount — boundary values like this are exactly where tests look.
Milestone · ●●●difficulty 3 of 5 · about 12 min
Milestone 4: multi-buy and order discounts
Add the two discounts. Use constants MULTI_BUY_QTY = 3, MULTI_BUY_RATE = 0.10, ORDER_DISCOUNT_FROM = 200 and ORDER_DISCOUNT_RATE = 0.05.
Multi-buy. After printing an item row whose quantity is 3 or more, calculate discount = round(line_total * MULTI_BUY_RATE, 2), subtract it from line_total before adding to subtotal, and print multi-buy -10% (two leading spaces) left-aligned in 31 with -discount formatted >10.2f.
Order discount. After printing the subtotal, set order_discount = 0; if subtotal >= ORDER_DISCOUNT_FROM, set it to round(subtotal * ORDER_DISCOUNT_RATE, 2) and print an Order discount 5% row showing -order_discount.
VAT becomes round((subtotal - order_discount) * VAT_RATE, 2) and total = subtotal - order_discount + vat.
With the four-item order from the brief (coffee, dates, 3 milk, 2 olive oil) the totals block reads:
Subtotal 201.74
Order discount 5% -10.09
VAT 15% 28.75
TOTAL 220.40
Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.
Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type
Output
Run your code to see its output, or Check to test it against the requirements.
The solution becomes available after you pass, or after all three hints and two checks.
Milestone 5: taking payment
After done, the till reads one more line: the cash handed over. Two outcomes are possible, and the receipt must show the right one. Round the change when you calculate it: 250 - 220.4 is 29.599999999999994 in floating point, and a till must say 29.60.
Milestone · ●●difficulty 2 of 5 · about 8 min
Milestone 5: payment and change
After the TOTAL row, read the cash paid with float(input()) into a variable paid and print a Paid row (label width 31, amount >10.2f). Then:
if paid is at least the total, set change = round(paid - total, 2) and print a Change row;
otherwise set short = round(total - paid, 2) and print Payment short by <short> with 2 decimals.
Example: for the four-item order and 250 paid, the last two rows are
Paid 250.00
Change 29.60
and with 200 paid they are
Paid 200.00
Payment short by 20.40
Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.
Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type
Output
Run your code to see its output, or Check to test it against the requirements.
The solution becomes available after you pass, or after all three hints and two checks.
Milestone 6: bad lines and empty orders
Real typing is messy. In the first hour of use Huda types Bananas, two, 4.50, Water 6pk, 1 and a line with a quantity of 0. With the current code each of these crashes the till: int("two") raises ValueError, and unpacking two fields into three names raises another.
Instead of letting Python decide, check each line before using it. A line is valid when:
it splits into exactly 3 fields;
the name is not empty after stripping;
the quantity is all digits (qty_text.isdigit()) and not 0;
the price is a number such as 6.20 or 24: removing at most one dot leaves only digits, price_text.replace(".", "", 1).isdigit().
Invalid lines are kept in a list and printed at the end so nothing is silently lost. This is called validating input: the program decides what it accepts instead of crashing on whatever arrives.
Milestone · ●●●difficulty 3 of 5 · about 15 min
Milestone 6: skip bad lines, handle an empty order
Make the till robust.
Create an empty list skipped before the loop. For every line that is not done, check it with the four rules above. Invalid lines are appended to skippedexactly as typed and do not count as items. Valid lines are processed as before.
If no valid items were entered, print No items entered. instead of the Items/totals/payment rows, and do not read a payment line.
At the very end, if any lines were skipped, print Skipped <n> line(s): followed by each skipped line indented by two spaces.
For the input Coffee beans, 2, 32.50, Bananas, two, 4.50, done, 100 the receipt ends:
Python code editor. Tab indents. Press Escape then Tab to leave the editor. Control or Command plus Enter runs the code; Shift plus Enter checks your answer.
Ctrl+Enter run · Shift+Enter check · your code is saved in this browser as you type
Output
Run your code to see its output, or Check to test it against the requirements.
The solution becomes available after you pass, or after all three hints and two checks.
Summary
A requirements brief becomes code by sorting it into inputs, calculations and outputs, then building one milestone at a time.
A sentinel loop with accumulators handles any number of input lines.
Business rules belong in named constants, and the order in which discounts and tax are applied is part of the requirement.
Format specifications such as {name:<18} and {amount:>10.2f} produce aligned, readable receipts.
Validating each line before converting it turns crashes into a clear list of skipped input.