Two anomalies in an evening-class grade script
A local council runs evening classes and uses a small Python script to turn raw marks into final grades. In the January run, two problems appeared. A student with marks 49.9, 50.0 and 50.1 was recorded as 'Fail', although the average is exactly 50.0 on paper. Separately, several students' result lists contained marks belonging to the student processed immediately before them. The code uses a function def add_result(mark, results=[]) that appends each mark and returns the list, then computes average = sum(marks) / len(marks) and tests if average >= 50. The resit window closes in four days, 240 records must be rechecked, and the registry requires a written, auditable explanation of each anomaly and the fix. The script runs once per enrolment session in a single Python process.
1. Explain the mechanism behind each anomaly, and state whether it would appear on every run. Use the specific marks to support your answer.
Read the worked answer
Anomaly 1 – shared default argument. results=[] is evaluated once, when the function object is created, not on each call. Every call in the same process appends to the same list, so student 2 receives student 1's marks and so on. It is not random: it must appear from the second student onwards, and the contamination grows with the number of records. Re-running the script reproduces it.
Anomaly 2 – binary floating point. 49.9, 50.0 and 50.1 have no exact binary representation. Their stored sum is approximately 149.99999999999997, and dividing by 3 gives 49.99999999999999, which fails >= 50. The paper average is 50.0, so the discrepancy is representation error, not a marking error. It is deterministic for these inputs and would recur on every run; it would not affect every student, only those whose computed average lands microscopically below a boundary.
The two faults are independent: fixing one leaves the other. That distinction matters for the audit note, because the registry will ask whether any other student could be affected by the same causes.
2. Write the corrected function and grading comparison, and outline the checks you would run across the 240 records before reissuing results.
Read the worked answer
Corrected code (marks parsed from strings or stored as integer tenths):
def add_result(mark, results=None):
if results is None:
results = []
results.append(mark)
return results
from decimal import Decimal, ROUND_HALF_UP
def grade(marks):
if not marks:
raise ValueError('no marks supplied')
total = sum(Decimal(str(m)) for m in marks)
avg = (total / Decimal(len(marks))).quantize(
Decimal('0.01'), rounding=ROUND_HALF_UP)
return 'Pass' if avg >= Decimal('50.00') else 'Fail'
Why this works: None is immutable, so a fresh list is created per call; Decimal operates in base 10, so 49.9 + 50.0 + 50.1 is exactly 150.0 and the average is exactly 50.00. ROUND_HALF_UP makes the rounding policy explicit rather than relying on round()'s banker's rounding.
Verification: unit-test repeated calls to prove isolation; test empty and single-mark inputs; test boundaries 49.94, 49.95, 49.99 and 50.00 against the agreed rounding rule; re-run all 240 records in a fresh process and diff old versus new outcomes; manually inspect every record within 0.1 of a grade boundary; log the interpreter version and a hash of the input file. Reissue only after the registry signs off the explanation.