Skip to content

← Seams, Modules, Packaging and Tooling step 21 of 36

Medium Primitives

Does this version satisfy this specifier?

Implement the comparison a resolver runs thousands of times per install.

def satisfies(version: str, specifier: str) -> bool:

version is a release like "1.4.2"; specifier is a comma-joined list of clauses like ">=1.2, <2". Return True only if every clause holds.

Restricted to release segments — no pre-releases, post-releases, dev releases, epochs or local versions. Support these operators:

Operator Meaning
== != >= <= > < ordered comparison of the release tuple
~= compatible release: >=V and the prefix of V with its last segment dropped must match

The two rules that make this more than string handling:

Compare numerically, segment by segment. "1.10" is newer than "1.9". A lexicographic comparison says the opposite and looks correct until a project reaches its tenth minor release.

Missing segments are zero. "1.0" and "1.0.0" are the same release, so satisfies("1.0", "==1.0.0") is True. Pad the shorter tuple before comparing; do not compare tuples of different lengths directly.

And the operator worth getting exactly right:

  • ~=1.4.2 means >=1.4.2 and ==1.4.* — it accepts 1.4.9, rejects 1.5.0.
  • ~=1.4 means >=1.4 and ==1.* — it accepts both.

The dropped segment is the one allowed to move, so the number of segments written changes the meaning. ~= with a single segment (~=1) is not meaningful; raise ValueError.

Why this matters in a real repository. This function is the difference between an upper bound that expresses what you tested and one that strands every downstream consumer. A library’s ranges are the only thing its users can see, and a consumer cannot loosen a bound you imposed — they can only pin you to an older release or fork you.

Types. Model a clause as a frozen dataclass with an operator field typed Literal["==", "!=", ">=", "<=", "~=", ">", "<"] and a release: tuple[int, ...]. Dispatch with match. Exhaustiveness only holds if the Literal lists every operator you parse — if you parse an eighth operator into a seven-member Literal, the checker cannot help you, and assert_never in the final arm is what makes that failure visible.

Your submission must pass mypy --strict.

Loading visualization…