{ } Programming

Semantic versioning: what version numbers really mean

Semantic versioning explained: MAJOR.MINOR.PATCH, breaking changes, 0.x versions, pre-releases and how to declare dependencies without nasty surprises.

A library moves from version 2.4.1 to 2.5.0, then to 3.0.0. What do these numbers tell us? If they follow semantic versioning, they tell us a lot: whether the upgrade is safe, whether it adds features, whether it requires changes to our code. It is a simple convention that, when respected, makes dependency management far more predictable.

The MAJOR.MINOR.PATCH structure

The semantic versioning specification, known as SemVer, calls for a three-part version number:

Part When it increases Example
MAJOR Changes incompatible with previous versions from 2.5.3 to 3.0.0
MINOR New compatible features from 2.5.3 to 2.6.0
PATCH Compatible bug fixes from 2.5.3 to 2.5.4

When one part is incremented, those to its right reset to zero: after 2.5.3, a new feature leads to 2.6.0, not 2.6.3.

The promise to library users is clear:

  • upgrading the PATCH should always be safe;
  • upgrading the MINOR should be safe and may offer new features;
  • upgrading the MAJOR may require changes to your own code.

What a breaking change is

The delicate point is understanding what breaks compatibility. SemVer is based on the concept of a public API: the set of functions, classes, parameters and behaviours the library declares it offers. Breaking changes include, for example:

  • removing or renaming a public function;
  • adding a required parameter to an existing function;
  • changing the type or format of a returned value;
  • changing a documented behaviour, such as the order of results or how an error is handled;
  • dropping support for a version of the language or operating system.

Compatible changes, and therefore MINOR ones, include adding a new function or an optional parameter whose default value preserves the previous behaviour.

There is a subtle case: fixing a bug that someone relied on. Technically it is a PATCH, but for anyone who depended on the wrong behaviour it is a break. In these cases it is worth documenting the change well and, if the impact is wide, considering a MAJOR version.

Deprecate before removing

A good project does not remove functions suddenly. The typical path:

  1. in a MINOR version the function is marked as deprecated: it keeps working but emits a warning, pointing to the alternative;
  2. the documentation and release notes explain how to migrate;
  3. in the next MAJOR version the function is removed.

In Python the warning is emitted with the warnings module:

import warnings

def load_data(path):
    warnings.warn(
        "load_data is deprecated, use read_data",
        DeprecationWarning,
        stacklevel=2,
    )
    return read_data(path)

0.x versions and pre-releases

According to the specification, versions with a MAJOR of zero, such as 0.8.2, indicate initial development: the public API is not considered stable and may change at any time. Using a 0.x library in production is possible, but you should expect breakage even between MINOR versions.

Pre-releases are marked with a hyphen and an identifier: 3.0.0-alpha.1, 3.0.0-beta.2, 3.0.0-rc.1. They have lower precedence than the final version: 3.0.0-rc.1 comes before 3.0.0. They let those who wish try out new features, without everyone else receiving them automatically.

Declaring dependencies

Semantic versioning becomes truly useful when you declare a project's dependencies. Package management tools let you specify ranges of acceptable versions:

Notation Typical meaning
==2.5.3 Exactly this version
>=2.5,<3 Any 2.x version from 2.5 onwards
~=2.5 (Python) Compatible with 2.5, so from 2.5 up to before 3.0
^2.5.3 (npm) From 2.5.3 up to before 3.0.0

A range such as >=2.5,<3 expresses trust in SemVer's promise: I accept fixes and new features, but not breaking changes. For applications, a lock file is then added that records the exact versions installed, so the range describes what is acceptable and the lock file what was actually tested.

SemVer is not a guarantee

Semantic versioning is an agreement between people, not an automatic check. A maintainer can make a mistake and publish a breaking change as a MINOR. That is why automated tests run on every dependency upgrade remain necessary, along with reading release notes, especially for the project's most important libraries.

It is also worth remembering that not all projects follow SemVer: some use calendar versioning, such as 2024.03, useful when the release date matters more than compatibility. What matters is that the project states which scheme it uses.

In short

  • SemVer uses three numbers: MAJOR for breaking changes, MINOR for compatible new features, PATCH for fixes.
  • Compatibility is judged against the public API the library declares.
  • Deprecate with a warning before removing, and remove only in a MAJOR version.
  • 0.x versions promise no stability; pre-releases come before the final version.
  • Declare dependency ranges consistent with SemVer, but still rely on tests and lock files.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. With degrees in computer science and psychology, he combines technical analysis with an understanding of the human factor.

Read also