Python Quest

Track 17 · Lesson 1 of 12

Project: shop receipt generator

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.

Output. A receipt like this one:

CEDAR CORNER MARKET
Item               Qty    Price     Total
-----------------------------------------
Coffee beans         2    32.50     65.00
Dates 500g           1    24.00     24.00
Milk 1L              3     6.20     18.60
  multi-buy -10%                    -1.86
Olive oil 1L         2    48.00     96.00
Items: 4
-----------------------------------------
Subtotal                           201.74
Order discount 5%                  -10.09
VAT 15%                             28.75
TOTAL                              220.40
Paid                               250.00
Change                              29.60

Acceptance criteria.

  1. Every item line shows the name, quantity, unit price and line total, in aligned columns.
  2. Buying 3 or more of the same item takes 10% off that line (a "multi-buy").
  3. If the subtotal after multi-buys is 200 SAR or more, a further 5% comes off the order.
  4. VAT of 15% is added to the amount after all discounts.
  5. Each discount and the VAT are rounded to 2 decimal places when they are calculated.
  6. The receipt shows what was paid and the change, or how much is still owed.
  7. 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:

PartWhat it is here
Inputitem lines until done, then the cash paid
Calculationline total = quantity × price; multi-buy discount; subtotal; order discount; VAT; total; change
Outputheader, 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

Coffee beans, 2, 32.50
Dates 500g, 1, 24.00
Milk 1L, 3, 6.20
done

the output is:

CEDAR CORNER MARKET
Coffee beans x2 @ 32.50 = 65.00
Dates 500g x1 @ 24.00 = 24.00
Milk 1L x3 @ 6.20 = 18.60
Items: 3

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.

Predict the output

print(f"{'Tea':<6}|{2:>4}|{11.5:>7.2f}|")
print(f"{'Cardamom pods'[:6]:<6}|{12:>4}|{3:>7.2f}|")

Milestone · ●●●difficulty 3 of 5 · about 10 min

Milestone 3: print aligned columns

Make the receipt line up. Add WIDTH = 41 and change the output to:

CEDAR CORNER MARKET
Item               Qty    Price     Total
-----------------------------------------
Coffee beans         2    32.50     65.00
Dates 500g           1    24.00     24.00
Milk 1L              3     6.20     18.60
Items: 3
-----------------------------------------
Subtotal                           107.60
VAT 15%                             16.14
TOTAL                              123.74
  • 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:

  1. Multi-buy comes first, per line: 10% off a line of 3 or more.
  2. The subtotal is the sum of the lines after multi-buy.
  3. The order discount (5% from 200 SAR) is worked out on that subtotal.
  4. 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.

  1. 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 skipped exactly as typed and do not count as items. Valid lines are processed as before.
  2. If no valid items were entered, print No items entered. instead of the Items/totals/payment rows, and do not read a payment line.
  3. 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:

TOTAL                               74.75
Paid                               100.00
Change                              25.25
Skipped 1 line(s):
  Bananas, two, 4.50

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.