Skip to content

Semantic Versioning (SemVer): Why Version Numbers Have Three Parts

Most software version numbers look like 1.6.11 — three numbers separated by dots. This isn’t an arbitrary naming choice; it follows a widely adopted convention called Semantic Versioning, or SemVer. This article looks at why version numbers are split into three parts, and what it actually takes to implement that convention correctly in code.

What MAJOR.MINOR.PATCH Each Mean

SemVer formats a version as MAJOR.MINOR.PATCH (for example, 1.6.11), and each position carries a distinct meaning:

  • MAJOR: incremented when you make a breaking change — something that could stop existing usage from working
  • MINOR: incremented when you add functionality in a backward-compatible way — existing usage keeps working
  • PATCH: incremented when you fix a bug in a backward-compatible way — no behavior-breaking side effects

Note: “backward-compatible” means code or usage written for an older version keeps working on the newer one. A breaking change is one where something that used to work stops working after the update.

What matters here is that these three numbers aren’t just a sequential counter — each position is assigned a specific meaning. Just by glancing at a version number, you can get a rough sense of whether an update is likely safe to apply immediately or whether it deserves a closer look first.

Why a Single Incrementing Number Isn’t Enough

Imagine version numbers were just a plain sequence: v1, v2, v3, and so on. Looking at a single number like that tells you nothing about whether the change behind it was a minor fix or a major overhaul. Users would have to read the release notes every single time just to figure out whether it’s safe to upgrade — the number itself carries almost no information.

Splitting the version into MAJOR.MINOR.PATCH is a way to make the number itself carry meaning. If only the PATCH digit moved, you can generally trust it’s safe to apply right away. If MAJOR moved, that’s a signal to check what changed before upgrading. The structure of the number becomes a form of communication in itself.

Why Version Comparisons Need to Be Numeric

This app stores its current version as VERSION = "1.6.11" in version.py, and compares it against the latest version published on the update server at startup to decide whether an update is available. This comparison step hides a classic pitfall that’s easy to overlook when working with SemVer.

def _is_newer(remote_version, current_version):
    """Compare versions using semantic versioning (X.Y.Z)"""
    try:
        remote = tuple(int(x) for x in remote_version.strip().split('.'))
        current = tuple(int(x) for x in current_version.strip().split('.'))
        return remote > current
    except (ValueError, AttributeError):
        return False

If this comparison were done as a plain string comparison ("1.10.0" > "1.9.0"), the result would actually come out wrong. Python’s string comparison walks character by character in lexicographic order, so "1.10.0" ends up evaluated as smaller than "1.9.0" — the first character 1 matches, but at the second character it’s comparing . against ., then 1 against 9, and 1 sorts before 9. Numerically, 1.10.0 is the newer version, but a naive string comparison gets it backwards.

_is_newer avoids this by splitting each version string on the dot, converting each segment to an integer with int(), and comparing the resulting tuples. Tuple comparison in Python evaluates elements left to right as numbers, so (1, 10, 0) > (1, 9, 0) correctly evaluates to True. Only by treating each of the three parts as an independent number — rather than as characters in a string — does the version ordering actually match its intended meaning. The visual ordering of the string and the semantic ordering of “which version is newer” are not always the same thing, and that distinction is the whole point of this design.

A Version Number Doesn’t Live in Just One Place

There’s a second practical challenge in applying SemVer in a real project: the version string rarely lives in just one file. It typically ends up duplicated across the installer configuration, a metadata file on the distribution server, and filenames referenced in download links. In this app, beyond version.py itself, the version string is also embedded separately in the installer-generation config, a distribution metadata file, and the download page’s link text.

Keeping all of these in sync by hand is a standing risk — it’s easy to forget one. This project actually hit that exact problem once: the installer configuration was left un-bumped while every other file moved forward, so the installer that got built still carried the old version number. To close that gap, we built a script that lists every file that needs updating and bumps them all together, plus a --check mode that verifies every file’s embedded version string actually matches the single source of truth in version.py.

python tools/bump_version.py 1.6.11 2026-06-11   # bump every target file at once
python tools/bump_version.py --check              # verify consistency only (exit 1 on mismatch)

--check is wired in as a gate before the build step runs. If any file’s version string drifts from what version.py declares, the build stops right there. Rather than relying on someone remembering to check every file by hand, the consistency check runs mechanically, every time.

Summary

Point Takeaway
MAJOR.MINOR.PATCH split Each digit encodes whether a change is breaking, additive, or a plain fix
vs. a plain counter A single incrementing number carries almost no information on its own
The string comparison trap Lexicographic comparison gets "1.10.0" < "1.9.0" wrong
Comparing as tuples Converting each segment to int() and comparing as tuples restores correct ordering
Version numbers, duplicated Version strings scattered across multiple files need a bump-and-verify script to avoid drift

Semantic Versioning isn’t just a naming convention — it’s a design choice to make the version number itself a communication tool for whoever is about to upgrade. Actually implementing that idea correctly in code means paying attention to details outside the convention itself, like numeric comparison correctness and keeping duplicated version strings in sync.