The Canteen Billing Loop That Runs One Step Too Far
In a Python practical exam at a Tiruchirappalli school, students write a billing program for the school canteen. Prices of items sold are kept in a list: prices = [120, 45, 300, 80, 0], where the final 0 is a sentinel meaning 'end of bill'. Quantities are in a parallel list qty = [2, 4, 1, 3, 0]. A student writes: i = 0; total = 0; while i <= len(prices): total = total + prices[i] * qty[i]; i = i + 1; print(total). The program stops with IndexError. A second student writes a function apply_discount(total) that prints the discounted amount but does not return it; the test cell shows None. A third student checks quantities only after the total has been calculated, so a negative quantity changes the bill before any error is reported. The teacher wants one corrected program that stops at the sentinel, validates that no quantity is negative, and returns the final payable amount after a 10% discount when the total exceeds 500.
1. Trace the loop and explain the exact cause and moment of the IndexError. Rewrite the loop so it stops at the sentinel and also cannot run past the list. State the correct total before discount.
Read the worked answer
prices and qty both have length 5, with valid indices 0–4. The condition i <= len(prices) is true for i = 5 because 5 <= 5; the body then evaluates prices[5] * qty[5], and index 5 is out of range, so IndexError occurs on the sixth iteration. The first five iterations add 120×2 = 240, 45×4 = 180, 300×1 = 300, 80×3 = 240 and 0×0 = 0, giving 960. Correct loop: i = 0; total = 0; while i < len(prices) and prices[i] != 0: total += prices[i] * qty[i]; i += 1. Putting the bounds check first prevents an error if the sentinel is missing; putting the sentinel check first stops before adding the zero row. A for loop with break is equally valid: for p, q in zip(prices, qty): if p == 0: break; total += p * q. The sentinel is a control signal, not a sale.
2. A function apply_discount(total) prints the result but the caller receives None. Explain why and rewrite it to return the payable amount for totals over 500 with a 10% discount, including a test for 960.
Read the worked answer
In Python a function without an executed return statement returns None. print() only writes text to the console; it does not send a value back to the caller. So payable = apply_discount(960) binds None, and later arithmetic such as payable + 5 raises TypeError. Rewrite: def apply_discount(total): if total > 500: return total * 0.90; return total. Then payable = apply_discount(960) gives 864.0. The threshold is strict (> 500), so a total of exactly 500 is not discounted; if the shop means '500 or more', use >=. Returning also makes the function testable: assert apply_discount(960) == 864.0 and assert apply_discount(400) == 400. If the discounted amount must be rounded to the nearest rupee, use round(total * 0.90) and state that choice, because 864.0 is exact here but other totals may not be.
3. Add quantity validation and a linear search that reports whether any price is above 250, without using max(). Explain where the validation must go and why.
Read the worked answer
Validation must run before the multiplication; otherwise a negative quantity has already changed the total. Inside the loop: if qty[i] < 0: raise ValueError(f'negative quantity at index {i}'); then add. Raising stops the bill and points to the bad row; an alternative is to skip the row and collect an error list, which is better for a batch report. Search: found = False; for p in prices: if p == 0: break; if p > 250: found = True; break. The sentinel must be excluded or the search may treat 0 as a price; 0 is not above 250 here, but the principle matters. For [120, 45, 300, 80, 0], found becomes True at 300. A linear search checks each element once, O(n), which is fine for a canteen list; max() would be shorter but the question asks for explicit traversal logic.