python
57 lines · 7 steps
Parsing and comparing semantic versions
A Version class parses semver strings with regex and orders them correctly, including prerelease precedence.
Explained by
highlit
1import re
2from functools import total_ordering
3from typing import Optional
4
5_SEMVER_RE = re.compile(
6 r"^(?P<major>0|[1-9]\d*)"
7 r"\.(?P<minor>0|[1-9]\d*)"
8 r"\.(?P<patch>0|[1-9]\d*)"
9 r"(?:-(?P<prerelease>[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
10 r"(?:\+(?P<build>[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$"
11)
12
13
14@total_ordering
15class Version:
16 def __init__(self, major, minor, patch, prerelease=None):
17 self.major = major
18 self.minor = minor
19 self.patch = patch
20 self.prerelease = prerelease
21
22 @classmethod
23 def parse(cls, text: str) -> "Version":
24 match = _SEMVER_RE.match(text.strip())
25 if not match:
26 raise ValueError(f"invalid semantic version: {text!r}")
27 return cls(
28 int(match["major"]),
29 int(match["minor"]),
30 int(match["patch"]),
31 match["prerelease"],
32 )
33
34 @staticmethod
35 def _prerelease_key(prerelease: Optional[str]):
36 if prerelease is None:
37 return (1,)
38 parts = []
39 for ident in prerelease.split("."):
40 if ident.isdigit():
41 parts.append((0, int(ident), ""))
42 else:
43 parts.append((1, 0, ident))
44 return (0, tuple(parts))
45
46 def _key(self):
47 return (self.major, self.minor, self.patch, self._prerelease_key(self.prerelease))
48
49 def __eq__(self, other):
50 if not isinstance(other, Version):
51 return NotImplemented
52 return self._key() == other._key()
53
54 def __lt__(self, other):
55 if not isinstance(other, Version):
56 return NotImplemented
57 return self._key() < other._key()
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Encoding comparison logic as a tuple key lets Python's built-in ordering do the hard work for you.
- 2@total_ordering derives the full set of comparison operators from just __eq__ and __lt__.
- 3Semver's rule that any prerelease is lower than its release needs deliberate handling, not naive string comparison.
Related explainers
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
python
from typing import Any, Sequence, Mapping def render_markdown_table(
Rendering an aligned Markdown table in Python
string formatting
data transformation
closures
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/parsing-and-comparing-semantic-versions-explained-python-9675/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.