This is a real book from the library, printed exactly as members get it. Six extended scenarios with support boxes, extension tasks and success criteria — questions complete, mark schemes held back for members.
Six extended coding scenarios that build one system across the whole unit — the Campus Café ordering and stock system. Each scenario has skeleton code to complete, a trace or debug task, and an exam-style question. In the members’ edition, full model solutions, completed traces and mark schemes sit in a teacher section at the back; this free sample carries the complete question pages, ready to print and hand out.
One scenario per lesson, in order — each continues the same system, and later scenarios reuse code from earlier ones. Each is designed for 50–60 minutes at a computer, with the written parts done away from the keyboard. Tasks carry an estimated time; the Extension is for early finishers and is not required. A Support box gives a way in for students who are stuck. Python 3 throughout; no HL-only parts in B2.
State / Identify — a short answer, no reasoning. Outline / Describe — give the steps. Explain — give reasons or causes. Construct / Write — produce working code. Trace / Determine the output — show every variable at every step. Deduce — work out the result or the fault, showing reasoning. Compare — matched criteria against each other. Suggest / Justify — choose and defend.
| Sc. | Title & focus | Revision booklet | Marks |
|---|---|---|---|
| 1 | Opening Up — variables, data types, sequence, selection | 1 · 2 | 30 |
| 2 | The Morning Rush — iteration, accumulators, validation loops | 2 | 32 |
| 3 | Splitting the Till — functions, parameters, return, scope | 3 · 4 | 30 |
| 4 | Stock Take — lists, 2-D lists, strings, dictionaries | 5 · 6 · 7 | 34 |
| 5 | When Things Go Wrong — exceptions, validation, stacks & queues | 8 · 9 | 32 |
| 6 | Closing the Books — Big O, searching, sorting, file processing | 10 · 11 · 12 · 13 · 14 | 36 |
| Total — full book | 194 | ||
The Campus Café is run by the sixth form as a student enterprise. It sells four items:
coffee $3.50 · tea $2.00 · juice $2.50 · muffin $1.75
Students on duty currently add up orders on paper and keep a tally of stock in a notebook. Takings are counted at the end of the week and often disagree with the stock used. Over the six scenarios you will build the café's ordering and stock system: pricing one order, then a whole morning, then breaking it into functions, then handling stock and receipts, then making it survive bad input, and finally reporting on a term's data held in a file.
All scenarios, prices and data in this book are fictional and written for this course. Mark allocations follow IB conventions but this is a teaching resource, not past-paper material.
Your first job is the simplest useful thing: price one order. The student on duty types how many coffees a customer wants; the program works out the cost and prints it. The café also has two rules:
• Staff get 20% off the whole order.
• An order of 6 or more coffees gets a further 50 cents off each coffee (a "meeting discount").
Coffee is $3.50. A quantity must be a whole number from 0 to 20.
(a) State the most appropriate data type for each of these, and give one reason for each choice. [6] ~6 min
| Value | Data type | Reason |
|---|---|---|
| number of coffees | ||
| price of one coffee | ||
| whether the customer is staff |
(b) Explain why input() alone cannot be used to collect the number of coffees, and state what must be done to the value. [3] ~4 min
(c) Complete the skeleton below so it prices one order and applies both discount rules correctly. [6] ~10 min
COFFEE_PRICE = 3.50
STAFF_DISCOUNT = 0.20
MEETING_SIZE = 6
MEETING_OFF = 0.50
qty = int(input("How many coffees? "))
is_staff = input("Staff? (y/n) ").lower() == "y"
unit = COFFEE_PRICE
if _______________________ : # meeting discount applies?
unit = _______________________
total = _______________________ # before the staff discount
if _______________________ : # staff?
total = _______________________
print("Total: $" + str(round(total, 2)))
(d) Deduce the total your completed code produces for 8 coffees bought by a staff member. Show your working. [3] ~4 min
(e) A student writes the price band code below. Deduce what it prints for a total of 25.00, and explain why. [4] ~5 min
if total > 5:
print("Small order")
elif total > 15:
print("Medium order")
elif total > 20:
print("Large order - free muffin!")
(f) Rewrite the code from (e) so it works correctly. [3] ~4 min
(g) The café adds a rule: an order qualifies for free delivery to the staff room if it is either over $20 or placed by a staff member, but only when there are at least 3 items. Write the condition as a single Python if, using brackets to make the precedence explicit. [3] ~4 min
(h) Exam-style. Explain why the prices should be stored as named constants rather than typed directly into the calculations. [2] ~3 min
Do the discounts in the order the café describes them. First decide the unit price: it is $3.50, unless the quantity reaches the meeting size, in which case it is 50 cents less. Only then multiply by the quantity to get the total. The staff discount applies to the whole total, so it comes last: taking 20% off means keeping 80%, so multiply by (1 - STAFF_DISCOUNT).
The café decides the two discounts must never combine — a customer gets whichever single discount is better for them, not both. Rewrite the pricing to calculate both possible totals and charge the lower. Then explain why this is harder to test than the original, and give the test case that distinguishes the two versions.
☐ I can choose the right data type and justify it ☐ I can convert input to a number ☐ I can write an if/elif/else chain in the correct order ☐ I can combine conditions with and/or and brackets ☐ I can spot a condition-order bug that produces no error message ☐ I can explain why named constants matter
Storing money as a float without rounding for display. Using = where == is needed. Applying the staff discount to the unit price and then again to the total. Ordering an if/elif chain from least restrictive to most, so later branches are unreachable. Forgetting int() around input().
My mark: ______ / 30 Marked by: ______________
One thing done well: ______________________
One thing to fix next time: ________________
One order at a time is not enough — the café serves a whole break time. The student on duty needs to enter order after order and see the morning's takings at the end. Nobody knows in advance how many customers there will be.
The duty student will type a coffee quantity for each customer, and -1 when the rush is over.
(a) Explain why a while loop is required here rather than a for loop. [3] ~4 min
(b) State what a sentinel value is, and explain why -1 is a suitable choice here but 0 would not be. [3] ~4 min
(c) Complete the skeleton so it accumulates the morning's takings and counts the customers. [6] ~10 min
COFFEE_PRICE = 3.50
SENTINEL = -1
takings = _______________ # initialise the accumulator
customers = _______________ # initialise the counter
qty = int(input("Coffees (-1 to finish): "))
while _______________________ :
takings = _______________________
customers = _______________________
qty = int(input("Coffees (-1 to finish): ")) # why is this here?
print("Customers:", customers)
print("Takings: $" + str(round(takings, 2)))
(d) Explain why the second input() must be the last statement inside the loop, and state what would happen if it were the first. [3] ~4 min
(e) Trace the completed loop for the inputs 2, 3, 1, −1. Complete every row. [6] ~9 min
| Pass | qty | qty != −1 ? | takings | customers |
|---|---|---|---|---|
| before loop | 2 | — | 0 | 0 |
| 1 | ||||
| 2 | ||||
| 3 | ||||
| exit |
(f) State the two lines printed at the end. [2] ~2 min
(g) This loop never ends. Identify the fault, explain why it happens, and write the corrected code. [4] ~5 min
i = 0
while i < 5:
print("Order", i)
(h) The café wants a receipt line printed for every individual coffee in every order (so an order of 2 prints two lines). Write the nested loop that does this for 3 orders, and state how many lines the inner loop body runs in total if the three orders are 2, 1 and 4 coffees. [4] ~7 min
(i) Exam-style. Distinguish between a count-controlled and a condition-controlled loop, giving one example of each from the café system. [3] ~4 min
Every "add up as you go" loop has the same three parts. Before the loop, set the total to 0 — not to the first value. Inside the loop, add this item's contribution to the total (takings = takings + qty * PRICE). After the loop, use the total. If your total comes out as the last order's value rather than the sum, you have put the initialisation inside the loop, where it resets every pass.
Extend the rush loop to also report the busiest single order and the mean order size. The mean needs a guard: explain what goes wrong if the duty student types -1 immediately, and write the code that prevents it.
☐ I can choose between for and while and justify it ☐ I can write a sentinel-controlled loop that reads before and inside ☐ I can use the accumulator pattern correctly ☐ I can complete a trace table for a while loop ☐ I can find and fix an infinite loop ☐ I can predict how many times a nested loop body runs
Initialising the accumulator inside the loop, so it resets each pass. Forgetting the second input(), giving an infinite loop. Counting the sentinel as a customer. Using range(len()) where a simple while is clearer. Assuming range(1, 5) includes 5.
My mark: ______ / 32 Marked by: ______________
One thing done well: ______________________
One thing to fix next time: ________________
The rush program works, but it is now 90 lines in one block, the discount calculation is copied in three places, and a second student has been asked to add card payments without breaking anything. The café supervisor asks for it to be "tidied into pieces".
You will break it into functions — and discover why that also makes it testable.
(a) Explain two problems caused by the discount calculation being copied in three places. [4] ~5 min
(b) Complete the function below. It must return the cost of an order, applying the meeting discount and the staff discount from Scenario 1. [6] ~10 min
COFFEE_PRICE = 3.50
STAFF_DISCOUNT = 0.20
MEETING_SIZE = 6
MEETING_OFF = 0.50
def order_cost(qty, is_staff=False):
"""Return the cost of qty coffees, applying any discounts."""
if qty _______________ 0: # guard: nothing ordered
return _______________
unit = COFFEE_PRICE
if _______________________ :
unit = _______________________
total = _______________________
if is_staff:
total = _______________________
_______________ round(total, 2) # hand the value back
print(order_cost(2))
print(order_cost(8, True))
(c) Distinguish between a parameter and an argument, using order_cost to illustrate your answer. [3] ~4 min
(d) State the purpose of is_staff=False in the definition, and state what order_cost(4) therefore calculates. [2] ~3 min
(e) A student rewrites the function to print the total instead of returning it. Explain two things this makes impossible. [4] ~5 min
(f) Deduce the output of this program and explain it with reference to scope. [4] ~6 min
takings = 100
def add_sale(amount):
takings = amount
print("inside:", takings)
add_sale(20)
print("outside:", takings)
(g) Rewrite add_sale so the caller's running total is correctly updated, without using a global variable. [3] ~5 min
(h) The supervisor asks for a recursive function that prints a countdown of the last 5 minutes before closing. Complete it, and state what would happen if the base case were removed. [4] ~6 min
def countdown(mins):
if _______________________ : # base case
print("Closed.")
_______________
print(mins, "minutes left")
countdown(_______________) # recursive case
countdown(5)
(i) Exam-style. The café system is now split into order_cost, take_payment and print_receipt. Explain two benefits of this structure for a second programmer joining the project. [4] ~6 min
Think of a function as a vending machine. return is the machine handing you the drink — you can then do anything with it: drink it, sell it, compare it. print is the machine shouting what it would have given you and keeping it. Only the first is useful to other code. Rule of thumb: a function should return its answer and let whoever called it decide whether to print it.
Write a set of five test calls to order_cost that between them cover normal, boundary and erroneous cases, each with the expected return value worked out by hand. Then explain why testing a function in this way is far easier than testing the 90-line version from Scenario 2.
☐ I can write a function with parameters and a return value ☐ I can use a default parameter ☐ I can explain the difference between a parameter and an argument ☐ I can explain why return beats print ☐ I can predict the effect of local vs global scope ☐ I can write a recursive function with a working base case
Forgetting return, so the function yields None. Assigning to a global inside a function and expecting the change to persist. Calling a function before it is defined. Missing the base case in recursion. Writing a function that does two jobs at once.
My mark: ______ / 30 Marked by: ______________
One thing done well: ______________________
One thing to fix next time: ________________
The café now sells all four items, and the supervisor wants stock tracked. Sales for the week are held in a 2-D list, one row per day, one column per item:
ITEMS = ["coffee", "tea", "juice", "muffin"]
PRICES = {"coffee": 3.50, "tea": 2.00, "juice": 2.50, "muffin": 1.75}
# rows = Mon, Tue, Wed, Thu, Fri columns = coffee, tea, juice, muffin
sales = [[24, 8, 5, 12],
[31, 6, 9, 15],
[19, 11, 4, 7],
[28, 9, 7, 18],
[35, 4, 11, 22]]
Customer names are typed by hand onto the order list and arrive inconsistently: " Amina ", "AMINA", "amina".
(a) State the value of sales[1][3], len(sales) and len(sales[0]), and say what each represents. [3] ~4 min
(b) Write code that prints the total items sold each day (one line per day). [3] ~5 min
(c) Complete the skeleton that totals each item across the whole week. Explain in one sentence why this needs the loops the other way round from part (b). [5] ~8 min
for col in range(_______________________): # one pass per ITEM
item_total = 0
for row in range(_______________________): # step down the DAYS
item_total = item_total + _______________________
print(ITEMS[col], item_total)
(d) Write code that calculates the café's total revenue for the week, using both sales and PRICES. [4] ~7 min
(e) Write code that finds the single busiest day (by items sold) and prints its index and total. [4] ~7 min
(f) A student writes best = 0 before searching for the busiest day, then compares totals. Explain why this happens to work here but is a bad habit, and state the safer way to initialise. [3] ~4 min
(g) Write a single line that turns any of " Amina ", "AMINA" or "amina" into the same comparable value, and state which two string methods you used. [3] ~4 min
(h) The order log holds lines like "Amina,coffee,2". Write code that splits one such line into its three parts and prints the cost of that order using PRICES. [4] ~6 min
(i) Explain why PRICES is a dictionary rather than a list, giving two reasons. [3] ~4 min
(j) Exam-style. A student writes backup = sales then edits backup[0][0], and finds sales has changed too. Explain why, and state the correction. [2] ~3 min
The index order is always sales[row][column] — day first, item second. A row total is easy: each row is a list, so sum(sales[r]) gives one day. A column total is harder: no single list holds it, so you must fix the column and step the row (sales[0][2], sales[1][2], sales[2][2]…). If your answer for one item looks like a day's total, you have the indices the wrong way round.
Build a frequency dictionary counting how many orders each customer has placed from a list of log lines, handling the inconsistent name formats so "AMINA" and " amina " count as the same person. Then print the customer with the most orders. Explain why get(name, 0) is needed.
☐ I can index a 2-D list correctly ☐ I can total a row and total a column ☐ I can combine a list and a dictionary in one calculation ☐ I can find a maximum and remember where it was ☐ I can normalise a string for comparison ☐ I can split a delimited line into fields ☐ I know why b = a does not copy a list
Swapping row and column indices. Using len(sales) where len(sales[0]) is needed. Forgetting int() on a field read from a split line. Comparing names without stripping and lower-casing. Assuming b = a copies a list. Initialising a maximum to 0 instead of to the first element.
My mark: ______ / 34 Marked by: ______________
One thing done well: ______________________
One thing to fix next time: ________________
On the first busy Friday the system crashed four times. The duty student's log:
• Typed "two" instead of 2 — program stopped.
• Asked for a "latte" — program stopped.
• Pressed Enter without typing anything — program stopped.
• Tried to split a $0 bill between 0 people — program stopped.
The café also wants two new features: orders must be served in the order they were placed, and the duty student wants an undo button for the last item added to an order.
(a) Name the exception raised by each of the four incidents above. [4] ~5 min
(b) Complete the robust input routine below. It must keep asking until a valid quantity (0–20) is entered, and must never crash. [6] ~10 min
MAX_QTY = 20
def get_quantity():
while True:
_______________: # start protecting
qty = int(input("How many? "))
_______________ ValueError: # not a whole number
print("Please enter a whole number.")
continue
if _______________________ : # range check
print("Must be between 0 and", MAX_QTY)
continue
_______________ qty # valid - hand it back
(c) Explain why a bare except: would be poor practice here. [3] ~4 min
(d) Name the validation check that would prevent each incident, and state the check's purpose. [4] ~5 min
| Incident | Check | Purpose |
|---|---|---|
| typed "two" | ||
| asked for "latte" | ||
| pressed Enter only | ||
| quantity of 500 |
(e) Identify the correct ADT for each new feature, state its discipline, and justify the choice from the café's wording. [4] ~6 min
(f) Complete the queue and stack operations. [5] ~8 min
order_queue = [] # customers waiting to be served
undo_stack = [] # items added to the current order
def join_queue(name):
order_queue._______________(name) # enqueue at the rear
def serve_next():
if _______________________ : # guard against underflow
return None
return order_queue._______________ # dequeue from the FRONT
def add_item(item):
undo_stack._______________(item)
def undo_last():
if len(undo_stack) == 0:
return None
return undo_stack._______________ # remove the most recent
(g) Amina, Ben and Chen join the queue in that order; one customer is served; Dina joins. State who is served next and who is at the rear. [2] ~2 min
(h) Define underflow, and explain why the guard in serve_next is necessary. [2] ~3 min
(i) Exam-style. Distinguish between validation and verification, and explain which one would not have prevented a duty student typing 3 coffees when the customer asked for 2. [2] ~3 min
Read the requirement and find the words that describe order of service. "Served in the order they were placed" — the first one in is the first one out, so FIFO, a queue. "Undo the last item" — the most recent one in is the first one out, so LIFO, a stack. Name the discipline explicitly in your answer; that is where the mark is.
Staff orders are supposed to jump the queue. Explain why a plain queue cannot do this, name the ADT that can, and describe what would have to change in serve_next. Then argue whether jumping the queue is a good idea for a café where students are watching.
☐ I can name the exception a given fault raises ☐ I can write try/except that never crashes and re-prompts ☐ I can explain why bare except: is wrong ☐ I can name and apply the six validation checks ☐ I can choose stack or queue from the required order of service ☐ I can guard against underflow ☐ I can distinguish validation from verification
Catching Exception or using a bare except:. Putting the range check inside the try where it is not needed. Using pop() instead of pop(0) for a queue. Forgetting the empty-structure guard. Saying "a stack is used for undo" without naming LIFO.
My mark: ______ / 32 Marked by: ______________
One thing done well: ______________________
One thing to fix next time: ________________
A full term of orders is now stored in orders.txt, one order per line, about 12 000 lines:
Amina,coffee,2,7.00 Ben,muffin,1,1.75 Chen,tea,3,6.00
The supervisor wants an end-of-term report: total revenue, the best-selling item, and a way to look up any single customer's spend quickly. She also mentions that the lookup "takes ages" in the current version, and that the report must not lose data if the file is missing.
(a) State the Big O complexity of each, and justify each in one clause. [4] ~5 min
| Operation | Big O | Justification |
|---|---|---|
reading orders[500] from a list | ||
| linear search for one customer | ||
| binary search of the sorted names | ||
| bubble sorting the 12 000 orders |
(b) Calculate the maximum number of comparisons a binary search needs on 12 000 sorted names, and show your reasoning. [3] ~4 min
(c) Complete the binary search. [5] ~9 min
def binary_search(names, target):
low = 0
high = _______________________
while low _______________ high:
mid = _______________________
if names[mid] == target:
return mid
elif names[mid] < target:
low = _______________________
else:
high = _______________________
return -1
(d) Trace the binary search for "Nadia" in the sorted list below. Complete every row. [5] ~8 min
index: 0 1 2 3 4 5 6 7 name: Amina Ben Chen Dina Eve Nadia Omar Sara
| Pass | low | high | mid | names[mid] | Action |
|---|---|---|---|---|---|
| 1 | 0 | 7 | |||
| 2 | |||||
| 3 |
(e) Explain why binary search would give the wrong answer if the names were not sorted. [2] ~3 min
(f) The supervisor asks why you do not "just always use binary search". Explain when a linear search is the better choice for the café. [3] ~4 min
(g) Complete one full pass of a bubble sort on the daily takings [41, 18, 37, 22], showing each comparison and whether a swap occurred. [4] ~6 min
| Comparison | List before | Swap? | List after |
|---|---|---|---|
| 41 vs 18 | [41, 18, 37, 22] | ||
(h) State which value is guaranteed to be in its final position after that pass, and explain why. [2] ~2 min
(i) Complete the file-reading routine. It must survive a missing file and skip any malformed line. [6] ~10 min
def load_orders(filename):
"""Return a list of (name, item, qty, cost) tuples."""
orders = []
try:
_______________ open(filename, "_______") as f: # read mode
for line in f:
line = line._______________() # remove newline
if line == "":
continue
parts = line.split("_______")
if len(parts) != 4:
print("Malformed line skipped:", line)
continue
orders.append((parts[0], parts[1],
_______________(parts[2]), # qty as a number
float(parts[3])))
_______________ FileNotFoundError:
print(filename, "not found - starting with no data")
return orders
(j) Explain why "a" must be used rather than "w" when adding a new order to orders.txt. [2] ~2 min
Count the loops, not the lines. No loop over the data means the work never grows — O(1). One loop through n items — O(n). A loop inside another, both over n — O(n²). If each step throws away half of what is left — O(log n). Two loops one after the other are still O(n): constants are dropped, so O(2n) is written O(n).
The supervisor wants customer lookup to be instant. Argue whether the right answer is (i) sort the names and binary search, or (ii) build a dictionary of name to total spend. Give the Big O of building and of looking up in each, and say which you would choose for 12 000 orders looked up 200 times a day — and why.
☐ I can state and justify the Big O of an operation ☐ I can calculate binary search comparisons from log₂n ☐ I can write and trace a binary search ☐ I can explain its sorted-data precondition ☐ I can complete a bubble sort pass ☐ I can read a file safely with with and handle a missing file ☐ I know the difference between "w" and "a"
Writing low < high instead of low <= high, so a single remaining element is missed. Forgetting // 2 and getting a non-integer index. Saying bubble sort is O(n) generally — that is the best case only. Forgetting .strip(), so the last field keeps its newline. Using "w" and silently erasing the term's data.
My mark: ______ / 36 Marked by: ______________
One thing done well: ______________________
One thing to fix next time: ________________