Practice problem
Class Average To Two Decimals
EasySolve in Python · graded against 2 sample tests and a hidden test set in your browser · Published
The Bug Report
The program reads a set of test scores and prints the class average to exactly two decimal places. It runs, it never raises an error, and the output is always formatted correctly — two digits after the point, every time. The digits themselves are wrong.
Input that shows the problem
4
10 20 30 45What the program prints
26.00What it should print
26.25The four scores add up to 105, and 105 divided by 4 is 26.25. The program agrees on the whole-number part
and then reports the fraction as nothing at all. Across other inputs the same thing shows up: the digits
after the decimal point are always 00, no matter what the scores are.
Input Format
The input has exactly two lines:
- Line 1: a single integer n, the number of scores.
- Line 2: n integers separated by single spaces, the scores themselves.
Output Format
Print one line containing the average, rounded to exactly two decimal places. Always print both decimal
digits, so an average of 5 prints as 5.00 and an average of 7.5 prints as 7.50.
How to Debug It
The most useful thing in the report is that the two decimals are always 00. That is not what a rounding
mistake looks like — a rounding mistake gets the last digit wrong occasionally. Digits that are always zero
means the fractional part is not being rounded badly, it is not arriving at all. Reproduce it with the
smallest input that must have a fraction: two scores whose total is odd, such as 7 8, where the answer
has to be 7.50.
Now decide which half of the program to suspect, because there are only two candidates: the function that
computes the average, and the format string that prints it. Test them separately rather than reading both.
Print the return value of average(scores) on its own line, with no formatting at all, right before the
formatted print. If the unformatted value already shows no fraction, the formatting is innocent and the
arithmetic is where the fraction is being lost. Confirm it the other way round too, by formatting a value
you make up: print(f"{7.5:.2f}") proves in one line that .2f can show a fraction when it is given one.
That narrows the search to a single expression — the one that divides the total by the count — and gives
you a hypothesis about what that expression returns. Check the type of its result, not just its value,
because a whole number and a whole-number-valued fraction print identically once .2f gets hold of them.
Change one operator, re-run 7 8, re-run the failing sample, then re-run the second sample, whose average
genuinely is a whole number, to be sure it still prints 5.00 and not 5.
What Was Wrong
The function ended with return total // len(scores).
In Python, // is floor division, not a cosmetic variant of /. Given two integers it performs the
division and then discards everything after the decimal point, rounding down towards negative infinity,
and it returns an int. So 105 // 4 is 26, not 26.25. The fraction was gone before the value ever
reached the print statement.
What made this so easy to miss is the .2f format specifier. Handed the integer 26, it faithfully
prints 26.00, because formatting a whole number to two decimal places is a perfectly reasonable thing to
do. The output therefore looks like a properly computed average with a fractional part that just happens
to be zero — every single time. The formatting did not hide a crash; it manufactured a plausible-looking
answer out of a value that had already lost its information.
The fix is one character, using true division:
return total / len(scores)/ between two integers in Python 3 always produces a float, keeping the fractional part, so 105 / 4
is 26.25 and .2f has something real to round. The second sample still prints 5.00, because 15 / 3
is the float 5.0 and the format specifier supplies the trailing zeros — which is where those zeros were
always supposed to come from.
Common Mistakes
- Keeping
//and trying to recover the fraction afterwards, for example by computing the remainder separately and pasting the digits on. It can be made to work, but it is a hand-rolled reimplementation of division that has to get rounding right on its own, and it will disagree with/on some input. Use the operator that was meant. - Reaching for
round(total / len(scores), 2)and printing that.roundreturns a number, not text, so an average of 5.0 prints as5.0and an average of 7.5 prints as7.5— one decimal digit, not the two the output format requires. Rounding and formatting are different jobs;.2fdoes both. - Assuming
//and/only differ on negative numbers. They differ on every division with a remainder. The direction of rounding is what changes for negatives; the loss of the fraction happens for positives just as completely.
Sample tests
Sample 1
Input
4 10 20 30 45
Expected output
26.25
The four scores total 105, and 105 divided by 4 is exactly 26.25.
Sample 2
Input
3 5 5 5
Expected output
5.00
The average is a whole number here, so the broken program prints the right thing by accident — the two trailing zeros come from the formatting, not from the arithmetic.
Your solution
Run tries the first sample. Submit grades against every sample and the hidden tests. Your code is saved in this browser as you go.
Press Esc then Tab to move keyboard focus out of the code editor.
Output will appear here after you run your code.