The Ranking Table That Changes Between Runs
For the regional robotics scrimmage, a Year 13 student wrote a Python 3 results program. Sixteen teams play a round robin; a win is worth 3 points, a draw 1, a loss 0. Teams are ranked by points, then by the result of the match between them. The program reads match results from a CSV, builds a dictionary of teams, and prints a ranked table. Two problems appeared at the trial run: two teams on 7 points swapped places between runs of the same unchanged data, and one team's win percentage displayed as 66.66666666666667%. The student's explanation was 'the sort is random'. The tournament is on Saturday, results must be shared with parents on the day, and the student now wants to add a live web display. Their test file happens to list teams in alphabetical order, which is also the order they finished in.
1. Explain why the ranking order changes between runs and describe how to make it deterministic.
Read the worked answer
Nothing in the sort is random; the input order is unstable.
Likely cause: teams are iterated from a set (or filtered out of a dict keyed by strings) and then sorted on points alone. Python randomises string hashing per process (PYTHONHASHSEED), so set iteration order differs between runs. sorted() is stable - equal keys keep their input order - so two teams on 7 points inherit whatever order the set produced that run. Same data, different printout.
Fix: sort on a full, explicit key that resolves every tie, for example:
sorted(teams, key=lambda t: (-t.points, -head_to_head(t), -t.point_diff, t.team_name))
Head-to-head must be looked up from the stored match results; it cannot be derived from the points total, and a three-way tie may need a mini-table of only those teams' matches.
Then write the rule down and publish it: points, head-to-head, point differential, name. A rule the coaches can read and check is worth more than one the program happens to implement.
Determinism is a hard requirement here: results go to parents on the day, so identical data must always produce an identical table.
2. Evaluate the student's test file and propose additional test cases, explaining what each one proves.
Read the worked answer
The existing test file is why the bug shipped: teams are listed alphabetically and finish alphabetically, so a tie broken by input order still prints in alphabetical order and looks correct. The test passes for the wrong reason.
Add cases with the expected table calculated by hand before running the program:
- Shuffled input: the same results in random order, run ten times including different
PYTHONHASHSEEDvalues, requiring identical output. Proves determinism. - Two-way tie on points separated by head-to-head. Proves the second criterion is actually applied, not just documented.
- Three-way tie where head-to-head cannot separate all three, forcing point differential and then name. Proves the fallback chain.
- Team with zero matches: win percentage divides by zero. Proves a crash cannot reach the venue.
- Withdrawn team whose matches were voided. Proves results are recalculated, not just filtered from the display.
- All draws, and a team that won every match. Boundary cases of the points system.
For each, write the expected ranking first. If expectations are computed from the program's output, the test proves nothing.
3. Advise the student on adding a live web display before Saturday and on how the percentage should be handled.
Read the worked answer
Advise against the web display this week, with a condition.
Reasoning: the ranking logic is not yet trusted - the trial-run bug is a correctness bug that a web page would publish live to parents. A web layer adds hosting, a server at school, network reliability and refresh logic, and debugging time, on a deadline that cannot slip. This is the classic risk of a new feature crowding out finishing and testing what already exists.
Better plan: extract the ranking into one function with the tests above, and have the program write the ranked table to a CSV or a single static HTML file that can be opened on a laptop at the venue and projected. That delivers 'results visible on the day' with almost no added risk.
On rounding: 66.66666666666667% should be formatted only at display, e.g. f"{wins/games*100:.1f}%". Store the exact counts and round once, at output - rounding stored values compounds and can reorder a leaderboard. If the tournament rules specify one decimal place for tie-breaks, use Decimal with ROUND_HALF_UP and apply the same rule everywhere so the printed table and the tie-break agree.
After Saturday, the web display is a sensible next project.