CS101 · Examination paper
2 hours 30 minutes · 100 marks · pass 40 · Merit 60 · Distinction 70 Closed book, one A4 sheet of handwritten notes. Section E runs on a laptop; no internet, no editor autocompletion, no AI tooling.
Time budget. A 20 marks / 30 min · B 16 marks / 25 min · C 32 marks / 50 min · D 16 marks / 25 min · E 16 marks / 20 min · checking 10 min.
Every answer in Sections A–D is a value, an expression, or a filled table. Eight of the 100 marks may be answered in sentences, and the paper says which.
Section A — What would Python display? (20 marks)
Section titled “Section A — What would Python display? (20 marks)”Write exactly what the interpreter prints. FUNCTION for a function value,
ERROR for an exception. No partial credit for a plausible answer.
A1. (4)
def f(x, acc=[]): acc.append(x) return acc
a = f(1)b = f(2)c = f(3)print(a) # (a)print(b is a) # (b)print(len(c)) # (c)print(f.__defaults__[0]) # (d)A2. (4)
x = print(1 or 2) # (a) what is printed by this line, and what x holdsy = 3 and 0print(x) # (b)print(y) # (c)print(0 or "" or "z") # (d)A3. (4)
rows = [[1], [2]]shallow = rows[:]shallow[0].append(9)print(rows) # (a)print(shallow[0] is rows[0]) # (b)print(shallow is rows) # (c)print(len(rows), len(rows[0]))# (d)A4. (4)
class Counter: n = 0 def __init__(self): self.n += 1
c1 = Counter()c2 = Counter()print(c1.n, c2.n, Counter.n) # (a)c3 = Counter()c3.n = 10print(c3.n, Counter.n, c1.n) # (b)A5. (4)
s = "error WARN error"print(s.count("error")) # (a)print(s.find("WARN")) # (b)print(s[6:10]) # (c)print(s[::-1][:3]) # (d)Section B — Execution model (16 marks)
Section titled “Section B — Execution model (16 marks)”B1. (8) Complete the frame table for the call make_fee(), then answer (d).
def make_fee(): rate = 0.18 fee = lambda gross: gross * rate rate = 0.20 return fee| Frame | Line just executed | locals | fee cell holds |
|---|---|---|---|
| f1 | rate = 0.18 | (a) ______ | (b) ______ |
| f1 | return fee | (c) ______ | — |
(d) print(make_fee()(10000)) displays ______.
(e) (4) Change one line so the same call displays 1800.0, write the changed
line, and name the mechanism that makes your version differ from the original.
B2. (4) Complete the call-stack table for div10(1000).
def div10(n): if n < 10: return 0 return 1 + div10(n // 10)| Depth | n at entry | Value returned by that frame |
|---|---|---|
| 1 | (a) ____ | (e) ____ |
| 2 | (b) ____ | (d) ____ |
| 3 | (c) ____ | (d) ____ |
| 4 | (c) ____ | (d) ____ |
(d) is the same value in three rows — write it once in each.
(f) (4) div10(42) returns ______ and div10(7) returns ______. State the
invariant of the recursive step, in one clause.
Section C — Write code (32 marks)
Section titled “Section C — Write code (32 marks)”C1. (8) Implement owed_pence(net_fee, paid) returning how much is still
owed, but never a negative number.
You may not use <, >, <=, >=, if, or else. No imports.
def owed_pence(net_fee: int, paid: int) -> int: """Return max(0, net_fee - paid), without comparison operators."""C2. (8) Two parts. Part (b) must call part (a).
(a) (2) Implement fee_rate(gross) returning 18 when gross >= 10000 and 15
otherwise, without if, else, or any comparison operator in a conditional
statement — the comparison may appear only as an arithmetic operand.
(b) (6) Implement fee_pence(gross) returning the fee in pence, calling
fee_rate, rounded half up, for any gross >= 0.
def fee_rate(gross: int) -> int: ...def fee_pence(gross: int) -> int: ...C3. (8) Journal lines look like
2026-10-30 11:02:01 ftm-backend[1]: started.
(a) (6) Write count_by_service(lines) returning {service: count} where
service is the third field with any [pid] removed.
(b) (2) State its time complexity and the reason, in one clause.
C4. (8) bisect_find(items, target) returns an index or -1.
(a) (6) Implement it over a sorted list. (b) (2) State the loop invariant in one sentence.
Section D — Diagnose (16 marks)
Section titled “Section D — Diagnose (16 marks)”D1. (8) This splits a gross amount into an 18% fee and the band’s share.
def split_fee(gross_pence): return round(gross_pence * 0.18), round(gross_pence * 0.82)(a) (4) Find every gross from 1 to 500 for which it is wrong. Write the count and the first four offenders. (b) (3) Name the root cause in one sentence. (c) (1) Write the corrected body.
D2. (8) This adds a tag to a band’s list of tags.
def add_tag(tags=[], tag=None): tags.append(tag) return tagsIt is called as add_tag(None, "booking") in one place and add_tag(tags, t) in
another.
(a) (4) Name every defect, one short phrase each. (b) (4) Write the corrected version, including a docstring with a precondition and a postcondition.
Section E — Practical, on the laptop (16 marks)
Section titled “Section E — Practical, on the laptop (16 marks)”Work in exam.py. Every answer must run. Paste real output where asked.
E1. (6) Write extract_refs(subject) returning every bracketed reference in
order, [] when there are none or the brackets are unmatched.
extract_refs("a [x] b [y]") → ["x", "y"].
E2. (6) Write three tests for it: a valid case, a boundary case, and one case you believe reveals a defect.
E3. (4) Paste the failure from the third test and state, in one sentence, whether the code or the expectation is wrong.
Mark scheme
Section titled “Mark scheme”Section A (4 marks each: 1 per blank)
Section titled “Section A (4 marks each: 1 per blank)”| (a) | (b) | (c) | (d) | |
|---|---|---|---|---|
| A1 | [1, 2, 3] | True | 3 | [1, 2, 3] |
| A2 | 1 printed, x is None (2 marks) | None | 0 | z |
| A3 | [[1, 9], [2]] | True | False | 2 2 |
| A4 | 1 1 0 | 10 0 1 | — | — |
| A5 | 2 | 6 | WARN | ror |
Verified by execution (verify_cs101_exam.py). Notes for markers:
- A1(a) is
[1,2,3], not[1]— the printed value is the live default list. - A2(a) awards 1 for “prints 1” and 1 for “x is None”.
- A4(a) is
1 1 0:self.n += 1creates an instance attribute, it does not touch the class attribute.
Section B
Section titled “Section B”B1. (a) rate → 0.18, fee → function (2) · (b) closure over rate, not
the value (2) · (c) rate → 0.20 (2) · (d) 2000.0 (2).
(e) 4 = 1 for the changed line fee = lambda gross, r=rate: gross * r, 1 for
“default argument”, 2 for why: the default is evaluated once at definition
and stores the value, whereas the closure stores the name and reads it at call
time.
B2. (a) 1000, (b) 100, (c) 10, (d) 0 (2 marks total) · (e) 3 (2).
(d) is 0 in the three bottom rows. (f) 4 = 1 for 3, 1 for 0, 2 for the
invariant: “each frame returns one more than the depth below it” or equivalently
“the recursion reduces n by a factor of ten, so depth is ⌊log₁₀ n⌋ + 1”.
Section C
Section titled “Section C”C1. 8 = 3 for max(0, net_fee - paid), 2 for not reversing the subtraction,
2 for returning 0 (not -200) on the negative case, 1 for no banned construct
used. A solution using and/or short-circuit is acceptable and scores full
marks: net_fee - paid if net_fee > paid else 0 is not, for using if and >.
C2. (a) 2 = 15 + 3 * (gross >= 10000). Accept 3 * (gross >= 10000) + 15
and 18 - 3 * (gross < 10000); a solution with if scores 0.
(b) 6 = 2 for calling fee_rate (not duplicating the threshold), 2 for
ROUND_HALF_UP, 1 for converting the rate to Decimal not float, 1 for
returning int. fee_pence(0) must not raise.
C3. (a) 6 = 2 for the field index, 2 for stripping [pid], 2 for one pass.
(b) 2 = O(n) time; the reason must name a single pass over the lines.
C4. (a) 6 = 2 structure, 2 integer division, 1 bounds, 1 narrowing both
branches. (b) 2 = “if target is present, its index lies in [lo, hi]”.
Section D
Section titled “Section D”D1. (a) 4 = 2 for the count 9, 2 for the first four 25, 125, 175, 225.
(b) 3 = the two shares are rounded independently, so the two errors do not
cancel and their sum can differ from the gross.
(c) 1 = fee = round(gross_pence * 0.18); return fee, gross_pence - fee — or the
Decimal/half-up version.
D2. (a) 4 = 1 each: mutable default shared across calls; None argument
raises AttributeError; returns the caller’s own list so it mutates caller
state; no validation that tag is not None.
(b) 4 = 1 signature/types, 2 docstring with pre and post, 1 immutability
(e.g. return tags + (tag,)).
Section E
Section titled “Section E”E1. 6 = 2 scanning both brackets, 2 collecting every pair in order, 1
skipping past a consumed pair, 1 returning [] on unmatched input without
raising.
E2. 6 = 3 tests that run and assert, 3 for a genuine boundary and a genuine
suspected defect (not three tautologies).
E3. 4 = 2 real pasted failure, 2 correct adjudication.
Grade descriptors
Section titled “Grade descriptors”| Band | The script |
|---|---|
| Distinction (70+) | Every prediction in A and B correct; the frame tables filled with the closure explanation in B1(e); C1–C4 correct with the constraints respected; D1 finds all 9 offenders by running, not guessing; E has a test that fails for the right reason. |
| Merit (60–69) | At most two blanks wrong; B1(e) changes the line correctly but explains with “it captures the value” without the timing; C2(b) right but rate built from a float; D1 finds the count but not the offenders. |
| Pass (40–59) | Prediction mostly right, one conceptual blank wrong (typically A1(a) as [1]); C1 solved with max; C4 implemented but the invariant stated as “it searches”; D2 finds the mutable default but not the aliasing. |
| Fail (<40) | Predictions wrong in a way that shows the model is missing (A1 answers [1]; A3 answers [[1],[2]]); C1 uses a banned construct; E does not run. |
Section floors. A must score ≥ 8/20 and C ≥ 12/32. A candidate who cannot predict execution or write a function has not met the module’s outcome, whatever the aggregate.