Skip to content

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 holds
y = 3 and 0
print(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 = 10
print(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)

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
FrameLine just executedlocalsfee cell holds
f1rate = 0.18(a) ______(b) ______
f1return 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)
Depthn at entryValue 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.


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.


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 tags

It 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.


(a)(b)(c)(d)
A1[1, 2, 3]True3[1, 2, 3]
A21 printed, x is None (2 marks)None0z
A3[[1, 9], [2]]TrueFalse2 2
A41 1 010 0 1——
A526WARNror

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 += 1 creates an instance attribute, it does not touch the class attribute.

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”.

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]”.

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,)).

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.


BandThe 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.