Versioning & SemVer — Junior Level¶
Roadmap: Release Engineering → Versioning & SemVer Your first contact with version numbers: reading them, bumping them, and pinning them in a dependency file.
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concept 1 — Anatomy of a Version Number
- Core Concept 2 — What Each Bump Means
- Core Concept 3 — Pre-release and Build Metadata
- Core Concept 4 — Where the Version Lives
- Core Concept 5 — Pinning a Dependency
- Real-World Examples
- Mental Models
- Common Mistakes
- Test Yourself
- Cheat Sheet
- Summary
- Further Reading
- Related Topics
Introduction¶
Focus: reading a version string correctly and choosing the right number to bump when you change code.
Every library, app, and container image you depend on carries a version string like 2.4.1. That string is a promise from the author to you: it tells you whether upgrading is safe, risky, or somewhere in between. Most of the rules you will meet come from Semantic Versioning (SemVer), a small specification that turns a three-number string into a contract.
By the end of this page you will be able to read a version like 1.4.0-rc.2+build.55, decide whether a code change deserves a patch, minor, or major bump, and pin a dependency in package.json, go.mod, requirements.txt, or Cargo.toml without breaking your build.
Prerequisites¶
Required - You can run shell commands (npm, pip, go, cargo) in a terminal. - You have edited a dependency manifest at least once (e.g. package.json). - Basic Git: you know what a tag is, even if you have not created one.
Helpful - Having broken a build by upgrading a dependency (the fastest teacher). - A sense of what an "API" is — the functions, flags, and outputs others rely on.
Glossary¶
| Term | Plain-English meaning |
|---|---|
| SemVer | Semantic Versioning — the MAJOR.MINOR.PATCH rule set most ecosystems follow. |
| Release | A specific, published version of your software. |
| Bump | Increasing a version number by one component. |
| Breaking change | A change that forces callers to edit their code to keep working. |
| Pre-release | An early, not-yet-final version, e.g. -alpha.1, -rc.2. |
| Build metadata | Extra info after + that does not affect which version is "newer". |
| Pin | Locking a dependency to an exact version. |
| Constraint / range | A rule like ^1.2.0 saying which versions are acceptable. |
| Tag | A Git label on a commit, usually named v1.2.3. |
Core Concept 1 — Anatomy of a Version Number¶
A SemVer version has three required numbers separated by dots:
2 . 4 . 1
│ │ │
│ │ └── PATCH → bug fixes only
│ └──────── MINOR → new features, backward-compatible
└────────────── MAJOR → breaking changes
Read it left to right, most-significant first. 2.4.1 is newer than 2.4.0, which is newer than 2.3.9, which is newer than 1.99.99. A bigger MAJOR always wins, no matter how big the other numbers are: 2.0.0 beats 1.50.7.
The numbers do not behave like decimals. 1.10.0 is newer than 1.9.0, because 10 is greater than 9. Each component is its own integer, not a digit after a decimal point.
Core Concept 2 — What Each Bump Means¶
The whole point of SemVer is that the number you bump tells your users what kind of change shipped.
| You changed... | Bump | Example |
|---|---|---|
| Fixed a bug, no API change | PATCH | 1.4.2 → 1.4.3 |
| Added a feature, old code still works | MINOR | 1.4.3 → 1.5.0 |
| Removed/renamed something, old code breaks | MAJOR | 1.5.0 → 2.0.0 |
Two rules trip up beginners:
- When you bump MINOR, reset PATCH to 0.
1.4.3+ a feature →1.5.0, not1.5.3. - When you bump MAJOR, reset MINOR and PATCH to 0.
1.5.0+ a break →2.0.0.
Concrete example. You maintain a small Go library:
// v1.2.0
func Greet(name string) string { return "Hi " + name }
// Added an optional second function — old callers unaffected → MINOR → v1.3.0
func GreetFormal(name string) string { return "Good day, " + name }
// Renamed Greet to Hello — every caller must edit → MAJOR → v2.0.0
func Hello(name string) string { return "Hi " + name }
Core Concept 3 — Pre-release and Build Metadata¶
Before a final release, you often publish test versions. SemVer supports this with a pre-release suffix after a hyphen:
1.0.0-alpha earliest, expect bugs
1.0.0-alpha.1 a numbered alpha
1.0.0-beta.2 feature-complete, still testing
1.0.0-rc.1 release candidate — final unless a bug appears
1.0.0 the real thing
The key precedence rule: a pre-release is always older than the same version without a suffix. So 1.0.0-rc.1 < 1.0.0. This is the opposite of intuition for some people — 1.0.0 is the finished product, so it sorts last.
Build metadata comes after a + and is ignored when comparing versions:
1.0.0+sha.aaa and 1.0.0+sha.bbb are considered the same version for ordering — the + part is informational only.
Core Concept 4 — Where the Version Lives¶
The version string is usually stored in one place per project and read from there everywhere else:
For many tools you also create a Git tag so the exact source for a release is recoverable:
Go is special: it has no version field in a file. The version is the Git tag, and the module path encodes the major version (more on that in higher tiers).
Core Concept 5 — Pinning a Dependency¶
When you depend on someone else's package, you state which versions you accept. The default operators differ per ecosystem:
# npm — caret means "compatible with 1.4.1": allows 1.x.x but not 2.0.0
npm install lodash@^1.4.1
# tilde means "patch updates only": allows 1.4.x but not 1.5.0
npm install lodash@~1.4.1
# exact pin — no updates at all
npm install lodash@1.4.1
# requirements.txt (pip / PEP 440)
requests==2.31.0 # exact
requests~=2.31.0 # >=2.31.0, <2.32.0 (compatible release)
requests>=2.31.0 # 2.31.0 or anything newer
For applications, prefer a lockfile (package-lock.json, Cargo.lock, poetry.lock, go.sum) committed to Git. The lockfile records the exact versions you actually installed, so teammates and CI build the same thing you did.
Real-World Examples¶
- A safe upgrade. Your app uses
express@^4.18.0. A new4.18.2ships with a bug fix.npm updatepicks it up; nothing in your code changes. That is a PATCH working as designed. - A scary upgrade.
react@17→react@18is a MAJOR bump. The render API changed; you must update yourReactDOM.rendercalls. SemVer warned you with the leading18. - A pre-release in the wild.
npm install next@canaryinstalls versions like15.0.0-canary.42, deliberately not picked up by^15.0.0constraints, so only people who opt in get them.
Mental Models¶
- The version is a label on a sealed box. Once published, the contents never change. To change anything, you ship a new box with a new label.
- MAJOR is a warning siren. When the first number changes, assume "read the migration notes before upgrading."
- The caret
^trusts MINOR and PATCH but not MAJOR. It says "give me new features and fixes, but never a breaking change."
Common Mistakes¶
- Treating versions as decimals. Thinking
1.9.0>1.10.0. Wrong —10 > 9. - Bumping PATCH for a new feature. Users who pinned
~1.4.0will never receive it, and you have lied about what changed. - Forgetting to reset lower components.
1.4.3+ feature should be1.5.0, not1.5.3. - Not committing the lockfile. "Works on my machine" bugs come straight from this.
- Editing a published version in place. Republishing
1.4.1with different contents breaks everyone's cache and trust. Always ship1.4.2.
Test Yourself¶
- Order these from oldest to newest:
1.0.0,1.0.0-rc.1,1.0.0-alpha,1.0.1. - You fixed a typo in an error message that some users match on in their tests. PATCH, MINOR, or MAJOR?
- What does
^2.3.0allow that~2.3.0does not?
Answers
1. `1.0.0-alpha` < `1.0.0-rc.1` < `1.0.0` < `1.0.1`. Pre-releases sort before the final release; final sorts before the next patch. 2. Technically a PATCH (bug fix), but if users depend on the exact text it can break them — a preview of why SemVer is a *social* contract, covered in the senior tier. The safe default is still PATCH, with a note in the changelog. 3. `^2.3.0` allows `2.4.0`, `2.9.1`, etc. (MINOR updates). `~2.3.0` only allows `2.3.x` (PATCH updates).Cheat Sheet¶
MAJOR.MINOR.PATCH
│ │ └ bug fix, no API change
│ └────── new feature, backward-compatible
└──────────── breaking change
PRE-RELEASE: 1.0.0-alpha.1 < 1.0.0-beta.1 < 1.0.0-rc.1 < 1.0.0
BUILD META: 1.0.0+sha.abc == 1.0.0+sha.def (ignored in ordering)
npm/cargo ^1.2.3 → >=1.2.3 <2.0.0 (minor + patch)
npm ~1.2.3 → >=1.2.3 <1.3.0 (patch only)
pip ~=1.2.3 → >=1.2.3 <1.3.0
exact pin 1.2.3 → only 1.2.3
Summary¶
- A SemVer version is
MAJOR.MINOR.PATCH; each part is its own integer. - Bump PATCH for fixes, MINOR for backward-compatible features, MAJOR for breaking changes — and reset the lower parts to zero.
- Pre-releases (
-rc.1) sort before the final release; build metadata (+sha) is ignored in ordering. - Store the version in one place and tag releases in Git.
- Use
^/~/==to control which updates you accept, and commit your lockfile.
Further Reading¶
- The Semantic Versioning spec (semver.org) — short and worth reading end to end.
- npm
semvercalculator (semver.npmjs.com) — paste a range, see what it allows.
Related Topics¶
- Changelogs & Release Notes — how to describe what changed in each bump.
- Registries & Distribution — where published versions live.
- Build Systems — where the version gets embedded into artifacts.
In this topic
- junior
- middle
- senior
- professional