Skip to content

Instantly share code, notes, and snippets.

@jpgoldberg
Created November 18, 2025 07:42
Show Gist options
  • Select an option

  • Save jpgoldberg/5b33586e13cf00063a3fa9d59330b0c9 to your computer and use it in GitHub Desktop.

Select an option

Save jpgoldberg/5b33586e13cf00063a3fa9d59330b0c9 to your computer and use it in GitHub Desktop.
Egregiously over-engineered score to grade module (Python)
"""Egregiously over-engineered test score to letter grade utility
Originally intended to illustrate how to do that grading program
using bisect, expanding on the example from the bisect documentation
https://docs.python.org/3/library/bisect.html#examples
But then I started tinkering way to much.
"""
# bisect.bisect() does the magic, but it takes some practice,
# and the documentation isn't really clear to those who haven't
# tried to do this on their own first.
from bisect import bisect
# Mapping is just so I can give a proper type annotation for grade()
# Don't worry about it at this point. It roughly means a
# dict-like thing that is NOT mutable.
# Likewise Sequence is for list-like things that are immutable.
from collections.abc import Mapping, Sequence
# Because we want math.inf for full range of possible scores
import math
# We will want to cache the __str__ result for a Grader instance
# But note that type checkers and @cache don't play nicely together.
# See https://stephantul.github.io/blog/cache/
import functools
class Grader:
"""Score to grade calculator.
Can be used for any bijective step function, Callable[[float], str].
"""
DEFAULT_MAP: Mapping[str, float] = {
"F": 59,
"D": 69,
"C": 79,
"B": 89,
"A": math.inf,
}
"""Default grade map."""
def __init__(
self,
mapping: Mapping[str, float] = DEFAULT_MAP,
min_score: float = -math.inf,
) -> None:
"""Defines the score to grade function.
:param mapping: Grade : Maximum score for that grade
:param min_score: The minimum possible score.
The maximum possible score will simply be the maximum value
in mapping.
:raises ValueError: if mapping has duplicate values.
:raises ValueError: if lowest mapping value is less than ``min_score``.
"""
self._min_score: float = min_score
# create dictionary sorted by values
self._mapping: Mapping[str, float] = dict(
sorted(mapping.items(), key=lambda item: item[1])
)
self._cutoffs = [v + 1 for v in self._mapping.values()]
if len(self._cutoffs) != len(set(self._cutoffs)):
raise ValueError("Score cutoffs must be unique")
if self._cutoffs[0] <= self._min_score:
raise ValueError(
f"lowest cutoff ({self._cutoffs[0]}) is less than "
f"minimum allowed score ({self._min_score})"
)
self._max_score = self._cutoffs[-1]
self._grades: Sequence[str] = list(self._mapping.keys())
def grade(self, score: float) -> str:
"""Returns the grade for a particular score.
:param score: The numeric score
:raises ValueError: if score exceeds max_score.
:raises ValueError: if score is less than min_score
:raises ValueError: if score in not finite
"""
if abs(score) == math.inf:
raise ValueError("Score must be finite")
if score < self._min_score:
raise ValueError(f"Score ({score}) < minimum ({self._min_score})")
if score > self._max_score:
raise ValueError(f"Score ({score}) > maximum ({self._max_score})")
# Now the magic bit
# idx will be the index to the the first member of the cutoffs
# array that that is not less than score
idx = bisect(self._cutoffs, score)
return self._grades[idx]
@property
def mapping(self) -> Mapping[str, float]:
return self._mapping
@functools.cache
def __str__(self) -> str: # pyright: ignore
s = f"Grade Mapping: {self._mapping}\n"
s += f"Minimum allowed score: {self._min_score}\n"
s += f"Maximum allowed score: {self._max_score}"
return s
from collections.abc import Mapping
import math
import sys
import unittest # for unittest.TestCase.subTest
import pytest
from dataclasses import dataclass
import grades
@dataclass(frozen=True)
class ScoreVector:
score: float
grade: str
exception: None | type[Exception] = None
note: str | None = None
class TestDefault(unittest.TestCase):
grader = grades.Grader()
vectors: list[ScoreVector] = [
ScoreVector(49, 'F', note="Normal"),
ScoreVector(50, 'F', note="Normal"),
ScoreVector(51, 'F', note="Normal"),
ScoreVector(59, 'F', note="Normal"),
ScoreVector(60, 'D', note="Normal"),
ScoreVector(61, 'D', note="Normal"),
ScoreVector(69, 'D', note="Normal"),
ScoreVector(70, 'C', note="Normal"),
ScoreVector(71, 'C', note="Normal"),
ScoreVector(79, 'C', note="Normal"),
ScoreVector(80, 'B', note="Normal"),
ScoreVector(81, 'B', note="Normal"),
ScoreVector(89, 'B', note="Normal"),
ScoreVector(90, 'A', note="Normal"),
ScoreVector(90, 'A', note="Normal"),
ScoreVector(0, 'F'),
ScoreVector(100, 'A'),
ScoreVector(-1, 'F', note="Below 0"),
ScoreVector(101, 'A', note="Above 100"),
ScoreVector(math.inf, 'A', exception=ValueError, note="Not finite"),
ScoreVector(-math.inf, 'F', exception=ValueError, note="Not finite"),
] # fmt: skip
def test_normal(self) -> None:
for v in self.vectors:
if v.exception is not None:
continue
if not (v.note is None or v.note == "Normal"):
continue
with self.subTest(msg=f'score: {v.score}. Note: "{v.note}"'):
grade = self.grader.grade(v.score)
assert grade == v.grade
def test_abnormal(self) -> None:
for v in self.vectors:
if v.exception is not None:
continue
if v.note is None or v.note == "Normal":
continue
with self.subTest(msg=f'score: {v.score}. Note: "{v.note}"'):
grade = self.grader.grade(v.score)
assert grade == v.grade
def test_exceptions(self) -> None:
for v in self.vectors:
if v.exception is None:
continue
with self.subTest(msg=f'score: {v.score}. Note: "{v.note}"'):
with pytest.raises(v.exception):
_ = self.grader.grade(v.score)
class TestGrader(unittest.TestCase):
mapping: Mapping[str, float] = {
"Highest": math.inf,
"B": 89,
"C": 79,
"D": 69,
"Lowest": 59,
}
def test_duplicate(self) -> None:
bad_mapping: dict = {k: v for k, v in self.mapping.items()}
bad_mapping["E"] = bad_mapping["D"]
with pytest.raises(ValueError):
_ = grades.Grader(bad_mapping)
def test_min_mismatch(self) -> None:
min_score = self.mapping["Lowest"] + 1
with pytest.raises(ValueError):
_ = grades.Grader(mapping=self.mapping, min_score=min_score)
# while this should not be an error
_ = grades.Grader(
mapping=self.mapping, min_score=self.mapping["Lowest"]
)
if __name__ == "__main__":
sys.exit(pytest.main(args=[__file__]))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment