44.6 Versioning: Semantic Versioning and Calver
The Importance of Versioning
Versioning is the cornerstone of a healthy software ecosystem. It is the primary mechanism by which you communicate changes—and their potential impact—to your users and to automated systems that depend on your package. A well-defined versioning scheme allows users to make informed decisions about when to upgrade, helps automated tools resolve dependencies correctly, and provides a clear historical record of your project’s evolution. Without a consistent versioning strategy, you risk creating “dependency hell,” where conflicting or breaking changes in your package inadvertently break other software that relies on it. Two dominant strategies have emerged for Python and the wider software world: Semantic Versioning (SemVer) and Calendar Versioning (CalVer).
Semantic Versioning (SemVer)
Semantic Versioning is a strict, number-based scheme formalized at semver.org. It is designed to convey meaning about the underlying code changes and is ideal for libraries and frameworks with a public, stable API where users are highly sensitive to breaking changes. A SemVer version is expressed as MAJOR.MINOR.PATCH (e.g., 2.1.13).
- MAJOR version (
X.y.z): Incremented when you make incompatible API changes. This is a signal to users that the upgrade will likely require changes to their own code. For example, renaming a public function or removing a deprecated module warrants a major version bump. - MINOR version (
x.Y.z): Incremented when you add functionality in a backward-compatible manner. Users can upgrade with the expectation that their existing code will continue to work. Adding a new function or introducing an optional parameter are typical minor changes. - PATCH version (
x.y.Z): Incremented when you make backward-compatible bug fixes. These releases should be low-risk and address issues without introducing new features. Fixing a crash or a logical error is a patch-level change.
A pre-release version can be appended with a hyphen (e.g., 2.1.0-alpha, 2.1.0-beta.1). This is a best practice for testing upcoming changes with a subset of your user base before a general release.
# Example of a version defined in a `pyproject.toml` file using Poetry.
# This clearly indicates a pre-release of a new major version.
[tool.poetry]
name = "my-awesome-library"
version = "3.0.0-beta.1" # Major new release, in beta testing.
description = "A library for doing awesome things"
authors = ["Your Name <you@example.com>"]
Calendar Versioning (CalVer)
Calendar Versioning incorporates a date or time component into the version string. It is often preferred for applications, services, tools, and libraries whose development is tied to a release schedule rather than the nature of API changes. CalVer is useful when the “freshness” of the version is a more important signal to users than API stability. There is no single CalVer standard, but a common format is YYYY.M.MICRO (e.g., 2024.4.1 for the first release in April 2024).
- Advantages: It is immediately clear how old a release is. It reduces pressure on maintainers to avoid a “major” version bump, as the calendar increment is natural and expected.
- Disadvantages: The version number itself does not convey information about breaking changes. This must be communicated through other means like changelogs and documentation.
Popular Python projects like Ubuntu (20.04, 22.04), Django (4.2, 5.0), and PyCharm (e.g., 2023.1) use variations of CalVer.
# Example of a version defined in a `setup.py` file (for setuptools).
# This uses a CalVer scheme of YY.MINOR.MICRO.
import datetime
from setuptools import setup
current_year = datetime.datetime.now().year
current_month = datetime.datetime.now().month
# This would create a version like 24.4.0
calver_version = f"{str(current_year)[-2:]}.{current_month}.0"
setup(
name="my-cli-tool",
version=calver_version,
packages=["mytool"],
entry_points={
'console_scripts': [
'mytool=mytool.cli:main',
],
},
)
Specifying the Version in Your Project
The version for your package must be defined in a single, authoritative location. Modern Python packaging with pyproject.toml (PEP 621) has made this straightforward. The [project] table should contain a version field. It is considered an anti-pattern to import your package in setup.py to get the version, as this can cause issues during installation if dependencies are not yet available.
# The correct, modern way to specify a version in `pyproject.toml`
[project]
name = "my-project"
version = "1.2.3"
dependencies = [
"requests >=2.25.0",
]
For more complex setups, especially if you want to avoid duplicating the version string in code (e.g., for a __version__ attribute), you can use dynamic metadata. The attr: directive allows you to read the version from a module attribute.
# Using dynamic versioning in `pyproject.toml` (PEP 621)
[project]
name = "my-project"
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "mypackage.__version__"}
# In your `mypackage/__init__.py` file
# This single source of truth is used by both the code and the build backend.
__version__ = "1.2.3"
Automating Version Management
Manually updating the version string in your configuration file is error-prone. Automation is a key best practice. Libraries like bump2version or bump-my-version are designed for this purpose. They can automatically increment the correct part of the version string across all necessary files (e.g., pyproject.toml, __init__.py, CHANGELOG.md) in a single atomic commit.
# Install the bumpversion tool
pip install bump-my-version
# Configure it via a `.bumpversion.cfg` or `pyproject.toml` file
# Then, to create a new release:
bump-my-version bump patch # bumps 1.2.3 -> 1.2.4
bump-my-version bump minor # bumps 1.2.3 -> 1.3.0
bump-my-version bump major # bumps 1.2.3 -> 2.0.0
Common Pitfalls and Best Practices
- Never Release the Same Version Twice: PyPI prohibits uploading distributions with the same version string. Once
1.5.0is released, that identifier is taken forever. If you make a critical mistake, you must yank the release and create a new version (e.g.,1.5.1). - Use Pre-release Versions for Testing: Releasing an
alpha,beta, orrc(release candidate) version allows you to distribute test builds to a limited audience without occupying a final version number. - Keep a Changelog: Whether you use SemVer or CalVer, the version number is only a summary. A detailed changelog (e.g., using Keep a Changelog conventions) is essential for documenting what actually changed in each release, including migration instructions for breaking changes.
- Choose a Scheme and Stick to It: Consistency is more important than the specific scheme. Choose one that fits your project’s ethos—SemVer for API-sensitive libraries, CalVer for user-facing tools or rapidly evolving projects—and apply it predictably.
- Automate the Process: Integrate version bumping into your release workflow. A typical CI/CD pipeline might trigger a version bump and release on every push to the
mainbranch, ensuring no manual step is forgotten.