The Marks Summary That Keeps Growing
The computer club secretary writes a Python program to summarise marks from a text file. Each line is meant to be 'name,marks', but some lines are blank and one line has 'absent'. The current code is:
def add_score(score, scores=[]): scores.append(score) return scores
def summary(lines): below = [] for line in lines: if line.strip() == '': continue name, mark = line.split(',') if mark == 'absent': continue all_scores = add_score(int(mark)) if int(mark) < 40: below.append(name) avg = sum(all_scores) // len(all_scores) print('Average:', avg) print('Below 40:', below)
summary(['Asha,72', 'Ravi,38', '', 'Meera,absent', 'John,91']) summary(['Sam,55', 'Tara,29'])
The secretary reports that the first call prints Average: 67 and Below 40: ['Ravi'], which looks correct. The second call prints Average: 57 instead of 42, and the list held by add_score now contains marks from both classes. The file also contains a line 'Kumar, A.,78' that crashes the program.
1. Trace both calls. Show the state of add_score's default list after each mark and explain why 57 appears.
Read the worked answer
First call: Asha 72 appends 72, so the default list is [72] and all_scores refers to it. Ravi 38 appends 38, giving [72, 38]; all_scores still refers to the same list. John 91 appends 91, giving [72, 38, 91]. The blank line and 'absent' are skipped. The average is (72 + 38 + 91) // 3 = 201 // 3 = 67, and below is ['Ravi'].
Second call: the default list is not reset because default arguments are evaluated once. Sam 55 appends 55, giving [72, 38, 91, 55]. Tara 29 appends 29, giving [72, 38, 91, 55, 29]. all_scores again refers to this same list. The sum is 285 and the length is 5, so 285 // 5 = 57. The correct second-class average would be (55 + 29) // 2 = 42. The below list is local to summary, so it still prints ['Tara']; the hidden shared list is the bug.
2. Rewrite add_score and summary to fix the mutable default, handle absent/blank lines, and avoid a divide-by-zero when no valid marks exist. Use a float average or explain your choice.
Read the worked answer
Use None as the default and create a new list inside the function. Keep the per-call scores in summary local, validate the mark, and guard the empty case.
def add_score(score, scores=None):
if scores is None:
scores = []
scores.append(score)
return scores
def summary(lines):
scores = []
below = []
for line in lines:
line = line.strip()
if not line:
continue
parts = line.split(',')
if len(parts) != 2:
continue
name, mark = parts[0].strip(), parts[1].strip()
if mark.lower() == 'absent':
continue
try:
m = int(mark)
except ValueError:
continue
scores.append(m)
if m < 40:
below.append(name)
if not scores:
print('No valid marks')
return
avg = sum(scores) / len(scores)
print(f'Average: {avg:.2f}')
print('Below 40:', below)
I used / rather than // because an average of marks is usually reported with a decimal. The guard prevents division by zero. The first call gives 67.00 and the second gives 42.00, with no shared state.
3. The line 'Kumar, A.,78' crashes with 'too many values to unpack'. Propose a parsing rule that handles names containing commas without breaking the two-field format, and give two test cases (one valid, one invalid) with expected behaviour.
Read the worked answer
The marks field has no commas, so split from the right exactly once: name, mark = line.rsplit(',', 1). This keeps 'Kumar, A.' as the name and '78' as the mark. Then strip whitespace and validate the mark with int().
Valid test: 'Kumar, A.,78' -> name 'Kumar, A.', mark 78; the student is included in the average and checked against 40. Invalid test: 'Rita,abc' -> rsplit gives name 'Rita' and mark 'abc'; int() raises ValueError, which is caught, the row is skipped or logged, and the program continues. A second invalid case, 'Solo', has no comma and should be skipped with a warning rather than crashing. This rule is safer than split(',') for names, but a full CSV parser is still better if fields can contain quoted commas or escapes.