Real Semantic Versioning
Posted on January 15, 2025Semantic version (SemVer) is possibly the most widely used software versioning scheme.
We all know how SemVer works: MAJOR.MINOR.PATCH
. The first number is for backward-incompatible changes, the middle number is for backward-compatible new features, and the last number is for backward-compatible bugfixes.
…it’s a shame how infrequently it actually seems to be used this way!
Backward incompatible changes on minor versions happen all the time. By far the most common example are deprecations, even amongst libraries that claim to follow SemVer. Meanwhile, major version bumps often correspond to something big-and-shiny happening. The major version is used for ✨marketing✨.
If you’ve been around software for long enough then I reckon the chances are good that you’ve seen many libraries use this approach already. 1 And to be clear, this isn’t a post complaining about a misuse of SemVer. Rather: I am writing this post because I think it might be worth acknowledging this popular reality, and giving it a name!
That reality – ‘RealSemVer’
It works like so:
MAJOR.MINOR.PATCH
Major version bumps may include backward-incompatible changes, and are typically used for advertising substantial (exciting!) changes to the library.
Minor version bumps may include backward-incompatible changes, and are typically used for new features or deprecations.
Patch version bumps must only include backward-compatible changes, and are typically used for bugfixes.
There is no special meaning assigned to MAJOR
or MINOR
versions 0
. 2
The definition of ‘backward (in)compatible’ is for each project to decide. For example breaking changes to private/undocumented/experimental APIs, or bugfixes that induce changes in behaviour, or breaking changes to the ABI, may all be considered ‘backward compatible’. 3
That’s it. 4 Much like SemVer before it:
This is not a new or revolutionary idea. In fact, you probably do something close to this already.
Should you use this?
For now I’m just some guy with a blog and an observation (and quite a lot of open-source software too).
But I think I might intentionally start using this for my software libraries. It seems to me that having a ‘marketing number’ is genuinely useful. There’s a reason so many library authors already use this, even if it’s not by this name. 5
At the very least now that I’ve written this post, I have a resource to point my coworkers to every time a library makes a breaking change in their minor version release, again! (‘Ah you see, this is a library that is actually following RealSemVer.') Indeed whilst I’ve been mulling this idea over for a while, I was inspired to write this post by a certain release from a certain library, which brought a lot of stress to these aforementioned coworkers, and which to protect the guilty shall not be named.
Many ‘big’ software projects I’ve come across seem to explicitly opt-out of SemVer in favour of some variant:
Rust/Cargo use a SemVer variant that assigns compatibility guarantees to
0.X.Y
and0.0.X
releases.NumPy and Lightning both use a SemVer variant that allows deprecations in minor releases. Python and PyTorch both also does something similar.
JAX use a SemVer variant that allows backward-incompatible minor releases, provided these require only ‘some effort’ to upgrade. (See EffVer below.)
I’ve tried to capture most of these use-cases with the ‘RealSemVer’ described in this article. ↩︎
SemVer allows anything-goes whilst the major version is
0
; in practice most projects treat this as0.INCOMPATIBLE.COMPATIBLE
. Rust/Cargo explicitly formalises this, and also acknowledges this slight discrepancy from the SemVer spec. ↩︎If this seems like a lot, then (a) this is the reality of what many projects do anyway, so it’s the goal of RealSemVer to capture this, and (b) projects may still reasonably decide that these kinds of changes do count as breaking for some fraction of their users, and communicate this by bumping the minor version. ↩︎
Plus, for brevity, all the other usual rules you’ve come to expect:
- Each number must be a nonnegative integer, such that
MAJOR.MINOR.PATCH
increases monotonically (but not necessary consecutively 6) between releases. - Releases can be ‘unreleased’/‘yanked’ to indicate that they have known flaws and should not be used, but once released their contents cannot be modified. Do a new release instead.
- Versions may be suffixed by
-
and then some sequence of[0-9A-Za-z-]
to indicate prereleases, and these have lower precedence than their associated normal release. - Versions may be further suffixed by
+
and then some sequence of[0-9A-Za-z-]
to indicate build metadata, and these have the same precedence as their associated normal release.
One could write a formal version of RealSemVer that includes all of these details as well. ↩︎
- Each number must be a nonnegative integer, such that
Indeed, I’m not the first to comment on the limitations of SemVer. I think I’d probably highlight the following, all of which influenced how I’ve written up RealSemVer:
ComVer, ‘Compatible Versioning’, uses a
INCOMPATIBLE.COMPATIBLE
version number. Append a.0
and you get SemVer. Prepend aMARKETING.
and you get RealSemVer. As compared to ComVer, I believe that the ‘marketing number’ of RealSemVer genuinely serves a useful purpose to consumers, for highlighting particularly interesting changes to a library, and that this is probably why ComVer hasn’t taken off. (Also, at this point I think using three numbers for versioning is ‘just how versioning is done’ for many people, and an assumption hardcoded in many places as well.)ZeroVer humorously asserts that one should follow SemVer with the
MAJOR
version forever held at0
, and provides a substantial list of important projects that do exactly this. The true purpose of ZeroVer seems to be to encourage SemVer projects to actually make a 1.0.0 release already (!) – however I believe its existence serves to highlights a real problem with SemVer, and one that RealSemVer attempts to acknowledge/fix.EffVer, ‘Intended Effort Versioning’ suggests that
MAJOR.MINOR.PATCH
versions be incremented according to how much effort is required to upgrade: ‘a large effort’, ‘some effort’, or ‘no effort’ respectively. This is very similar to RealSemVer: they both offer the same compatibility guarantees, and both seem to be an attempt to canonicalise how SemVer really gets used. My gut feeling is that ‘marketing’ is a more realistic proxy than ‘effort’ for the intent of many library authors today, but as a practical matter I think these two versioning schemes can easily be good friends.
Footnotes in footnotes! Larger increments between versions, such as
3.1.4 → 3.1.6
sometimes occur for a variety of reasons, like un-releasing a version that is discovered to introduce serious bugs, or because somewhat-stable in-development versions are sometimes assigned a version number to ease testing amongst its community of developers. Note that this non-consecutivity is actually another slight difference to SemVer, which mandates that, for example, the minor version is set to0
after bumping the major version. ↩︎