Python Semantic Versioning — Core Concepts

What Semantic Versioning Promises

Semantic Versioning (semver) assigns meaning to version numbers in the format MAJOR.MINOR.PATCH:

  • MAJOR bumps signal incompatible API changes
  • MINOR bumps add functionality in a backward-compatible way
  • PATCH bumps fix bugs without changing the API

This is a social contract between library authors and consumers. When authors follow it, consumers can write dependency constraints like >=1.2,<2 with confidence that nothing will break until the next major release.

Python’s Version Scheme: PEP 440

Python does not enforce semver. Instead, PEP 440 defines a flexible version format that supports semver but also allows other schemes:

1.2.3          # standard release
1.2.3a1        # alpha pre-release
1.2.3b2        # beta pre-release
1.2.3rc1       # release candidate
1.2.3.post1    # post-release (metadata fix, no code change)
1.2.3.dev4     # development release

Most Python libraries that follow semver use the MAJOR.MINOR.PATCH subset, plus pre-release tags when needed.

When to Bump What

Patch (1.2.3 → 1.2.4)

  • Fixed a bug in existing behavior
  • Improved error messages
  • Performance improvements with no API change
  • Documentation-only changes (some teams skip the bump entirely)

Minor (1.2.3 → 1.3.0)

  • Added a new function, class, or parameter
  • Deprecated an existing feature (but did not remove it)
  • Added a new optional dependency

Major (1.2.3 → 2.0.0)

  • Removed a public function or class
  • Changed a function signature in a breaking way
  • Dropped support for a Python version
  • Changed default behavior that existing code relies on

The 0.x Escape Hatch

Versions below 1.0.0 carry an implicit warning: “This API is unstable.” During 0.x development, even minor bumps can contain breaking changes. Many Python libraries (FastAPI was 0.x for years) use this to iterate quickly without semver’s strict promises.

The convention is: graduate to 1.0.0 when your public API is stable enough that breaking it would hurt real users.

Version Constraints in Practice

ConstraintMeaningSemver Trust Level
>=1.2,<2Any 1.x from 1.2 upHigh — trusts minor/patch
~=1.2Same as above (PEP 440)High
>=1.2.3,<1.3Only 1.2.x patchesMedium — trusts patches only
==1.2.3Exact pinNone — no trust, no flexibility

For applications, exact pins in lock files provide reproducibility. For libraries, flexible constraints let consumers resolve their full dependency tree without conflicts.

Deprecation as a Bridge

Good semver practice never surprises users. Before removing a feature in a major release:

  1. Minor release — mark it deprecated with a DeprecationWarning
  2. Documentation — explain the replacement
  3. At least one minor cycle — give users time to migrate
  4. Major release — remove the deprecated code

Python’s warnings module makes this straightforward:

import warnings

def old_function():
    warnings.warn("old_function is deprecated, use new_function instead", DeprecationWarning, stacklevel=2)
    return new_function()

Common Misconception

“Semver means my code will never break on upgrade.” Semver is a promise, not a guarantee. Bugs in the implementation, undocumented behavior changes, and transitive dependency shifts can still cause breakage. Semver reduces risk; it does not eliminate it. Always run your test suite after upgrading.

Single Source of Truth for Version

Store your version in one place. The modern approach is pyproject.toml:

[project]
name = "my-package"
version = "1.3.0"

If you need runtime access, use importlib.metadata:

from importlib.metadata import version
__version__ = version("my-package")

Avoid duplicating the version string in __init__.py, setup.py, and pyproject.toml — they will inevitably drift.

The one thing to remember: Semver is a communication protocol between authors and users — follow it honestly, and your package becomes one that people trust to upgrade.

pythonversioningpackaging

See Also