Skip to content

← Everyday Standard Library Tools step 37 of 55

Medium Primitives

Sort Version Strings with Pre-Releases First

A release page lists versions such as 1.2.3-alpha, 1.2.3 and 1.10.0, and must sort them correctly. Write a SemVer class with @total_ordering, then report on some comparisons. This exercise uses a simplified version format, not the full Semantic Versioning standard.

versions = [SemVer.parse(text) for text in ["1.2.3", "1.2.3-alpha", "1.10.0"]]
[str(version) for version in sorted(versions)]
# ["1.2.3-alpha", "1.2.3", "1.10.0"]

The format

Each input is major.minor.patch, optionally followed by -tag. The three numbers are integers, 0 or more, with no leading zeros. A tag is one or more ASCII letters, digits, dots and hyphens. There is no build metadata, and every input is valid.

What to write

@total_ordering
class SemVer:
    def __init__(self, major: int, minor: int, patch: int, pre: str = "") -> None: ...
    @classmethod
    def parse(cls, text: str) -> "SemVer": ...
    def __str__(self) -> str: ...
  • Parsing "1.2.3" stores an empty tag, and parsing "1.2.3-rc.1" stores "rc.1". Converting a parsed version with str() gives back its original text.
  • Compare (major, minor, patch) as numbers first. With the same numbers, a pre-release comes before the release, so 1.2.3-alpha < 1.2.3. Two tags compare as plain strings, so rc.10 < rc.2 here. Two versions are equal when the numbers and the tag all match.
  • Use @total_ordering with __eq__ and one ordering method, such as __lt__.
  • Return NotImplemented when the other operand is not a SemVer.
  • Make instances hashable, by hashing the same values that equality compares. Do not change the fields after construction.

The report

Write solve(versions: list[str], pairs: list[list[str]]) -> dict[str, object]. Each pair holds two version strings. Return these keys:

Key Value
sorted Every version in ascending order, as strings, duplicates kept
comparisons One list per pair, in order: [left < right, left <= right, left > right, left >= right, left == right, left != right]
eq_with_str SemVer(1, 0, 0) == "1.0.0", which is False
lt_with_str "TypeError" if SemVer(1, 0, 0) < "1.0.0" raises TypeError, otherwise "compared"
distinct The size of a set of the parsed versions

For versions ["1.2.3", "1.2.3-alpha"] and the pair ["1.2.3-alpha", "1.2.3"], sorted is ["1.2.3-alpha", "1.2.3"], the comparison row is [True, True, False, False, False, True], and distinct is 2.

Edge cases

  • Empty versions and pairs lists are valid.
  • Equal versions count once in distinct.

The starter's @dataclass(order=True) compares the raw fields in order, so the empty tag of a release sorts before any pre-release tag. Replace that with a comparison key that puts pre-releases first. The tests check only behavior, but the lesson is total_ordering.