Home/Blog/Python Package Versioning: SemVer, CalVer, and Best Practices
Software Engineering

Python Package Versioning: SemVer, CalVer, and Best Practices

Master Python package versioning with semantic versioning, calendar versioning, pre-release versions, and automated version bumping. Complete guide to changelog management and version constraints.

By InventiveHQ Team

Proper versioning communicates to users what changes to expect, helps dependency resolution work correctly, and makes your package's history understandable. This guide covers versioning strategies, tools, and best practices for Python packages.

Understanding Version Numbers

A version number tells users about compatibility and change magnitude:

┌─────────────────────────────────────────────────────────────┐
│                    Version Anatomy                           │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│        1    .    2    .    3    a1                          │
│        │         │         │     │                          │
│        │         │         │     └── Pre-release (optional) │
│        │         │         │                                 │
│        │         │         └── PATCH: Bug fixes             │
│        │         │                                           │
│        │         └── MINOR: New features (backward compat)  │
│        │                                                     │
│        └── MAJOR: Breaking changes                          │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Semantic Versioning (SemVer)

Semantic Versioning is the most common scheme for libraries. The format is MAJOR.MINOR.PATCH:

Version Component Meanings

ComponentIncrement WhenExample
MAJORBreaking API changes1.0.02.0.0
MINORNew features (backward compatible)1.0.01.1.0
PATCHBug fixes (backward compatible)1.0.01.0.1

What Counts as a Breaking Change?

# Breaking changes (increment MAJOR):

# Removing a public function
def calculate(x):  # removed → MAJOR bump
    pass

# Changing function signature
def process(data):           # before
def process(data, strict):   # after (required param) → MAJOR bump

# Changing return type
def get_value() -> int:      # before
def get_value() -> str:      # after → MAJOR bump

# Changing exception types
def load():
    raise ValueError()       # before
    raise TypeError()        # after → MAJOR bump
# Non-breaking changes (MINOR or PATCH):

# Adding optional parameter
def process(data, strict=False):  # MINOR (new feature)

# Adding new function
def new_feature():                # MINOR

# Fixing bug in existing function
def calculate(x):
    return x + 1  # was x + 2 (bug) → PATCH

The 0.x Exception

Before version 1.0.0, anything goes:

0.1.0 → 0.2.0  # Can have breaking changes
0.9.0 → 0.10.0 # Still unstable
0.99.0 → 1.0.0 # Now you're committing to stability

Use 0.x versions while your API is experimental. Reach 1.0.0 when you're confident in your API.

Calendar Versioning (CalVer)

CalVer uses dates in version numbers. It works well for:

  • Applications (not libraries)
  • Projects with regular releases
  • When "breaking changes" isn't meaningful

Common CalVer Formats

FormatExampleUsed By
YYYY.MM.DD2026.01.15Daily releases
YYYY.MM2026.01Monthly releases
YY.MM26.01Shorter format
YYYY.MINOR2026.1Year + sequence

Real-World CalVer Examples

# Ubuntu: YY.MM
20.04, 22.04, 24.04

# pip: YY.MINOR
23.0, 23.1, 23.2, 24.0

# Black (formatter): YY.MM.MICRO
23.1.0, 23.3.0, 23.7.0

Implementing CalVer

# pyproject.toml
[project]
name = "my-app"
version = "2026.01.0"  # Year.Month.Release

For automatic CalVer, use setuptools-scm with a custom scheme.

Pre-Release Versions

Pre-releases let users test upcoming versions without affecting stable installs.

Pre-Release Types

1.0.0a1   # Alpha: early testing, unstable
1.0.0a2   # Alpha 2
1.0.0b1   # Beta: feature complete, testing
1.0.0b2   # Beta 2
1.0.0rc1  # Release candidate: almost ready
1.0.0rc2  # Release candidate 2
1.0.0     # Final release

Sort Order

PEP 440 defines the sort order:

1.0.0.dev1 < 1.0.0a1 < 1.0.0b1 < 1.0.0rc1 < 1.0.0 < 1.0.0.post1

Installing Pre-Releases

# By default, pip ignores pre-releases
pip install my-package        # Gets 1.0.0 (stable)

# Explicitly request pre-releases
pip install --pre my-package  # May get 1.1.0a1

# Pin to specific pre-release
pip install my-package==1.1.0a1

Development and Post Versions

Development Versions

For unreleased code between versions:

1.0.0.dev1    # Development version
1.0.0.dev2
1.0.0.dev10
1.0.0         # Released version

Used by setuptools-scm for commits after a tag:

v1.0.0 tag → version 1.0.0
3 commits later → version 1.0.1.dev3+g1a2b3c4

Post Versions

For packaging-only fixes (no code changes):

1.0.0        # Original release
1.0.0.post1  # Fixed README or classifiers
1.0.0.post2  # Another packaging fix

Use sparingly—a new patch version is usually clearer.

Dynamic Versioning with setuptools-scm

setuptools-scm derives version from git tags, eliminating manual version management:

Setup

# pyproject.toml
[build-system]
requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"

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

[tool.setuptools_scm]
# Version written to file for runtime access
version_file = "src/my_package/_version.py"

How It Works

# Tag a release
git tag v1.2.3
git push --tags

# Build package
python -m build
# → my_package-1.2.3-py3-none-any.whl

# After more commits (no tag)
python -m build
# → my_package-1.2.4.dev2+g1a2b3c4-py3-none-any.whl

Accessing Version at Runtime

# src/my_package/__init__.py
try:
    from ._version import version as __version__
except ImportError:
    __version__ = "unknown"

Version Bumping Tools

bump2version (bumpversion)

Automates version updates across files:

# .bumpversion.cfg
[bumpversion]
current_version = 1.2.3
commit = True
tag = True

[bumpversion:file:pyproject.toml]
[bumpversion:file:src/my_package/__init__.py]
# Install
pip install bump2version

# Bump patch: 1.2.3 → 1.2.4
bump2version patch

# Bump minor: 1.2.3 → 1.3.0
bump2version minor

# Bump major: 1.2.3 → 2.0.0
bump2version major

tbump

Modern alternative with TOML config:

# tbump.toml
[version]
current = "1.2.3"

[git]
push = true
tag_template = "v{new_version}"

[[file]]
src = "pyproject.toml"

[[file]]
src = "src/my_package/__init__.py"
search = '__version__ = "{current_version}"'
replace = '__version__ = "{new_version}"'
pip install tbump

# Bump to specific version
tbump 1.3.0

poetry version

If using Poetry:

# Bump patch: 1.2.3 → 1.2.4
poetry version patch

# Bump minor: 1.2.3 → 1.3.0
poetry version minor

# Bump major: 1.2.3 → 2.0.0
poetry version major

# Set specific version
poetry version 2.0.0

# Pre-release
poetry version prepatch  # 1.2.3 → 1.2.4a0

Changelog Management

A changelog documents what changed between versions.

Keep a Changelog Format

The standard format from keepachangelog.com:

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- New feature X

## [1.2.0] - 2026-01-15

### Added
- Support for Python 3.13
- New `process_async()` function

### Changed
- Improved performance of `calculate()` by 50%

### Deprecated
- `old_function()` will be removed in 2.0.0

### Fixed
- Fixed memory leak in long-running processes
- Corrected typo in error message

## [1.1.0] - 2025-12-01

### Added
- Initial async support

Change Categories

CategoryDescription
AddedNew features
ChangedChanges to existing functionality
DeprecatedFeatures that will be removed
RemovedFeatures that were removed
FixedBug fixes
SecuritySecurity vulnerability fixes

Automating with Towncrier

Towncrier generates changelogs from fragment files:

# pyproject.toml
[tool.towncrier]
directory = "changelog.d"
filename = "CHANGELOG.md"
start_string = "<!-- towncrier release notes start -->\n"
underlines = ["", "", ""]
template = "changelog.d/template.md"
title_format = "## [{version}] - {project_date}"
issue_format = "[#{issue}](https://github.com/user/repo/issues/{issue})"

[[tool.towncrier.type]]
directory = "added"
name = "Added"
showcontent = true

[[tool.towncrier.type]]
directory = "changed"
name = "Changed"
showcontent = true

[[tool.towncrier.type]]
directory = "fixed"
name = "Fixed"
showcontent = true

Create fragment files for each change:

# changelog.d/123.added
Added support for Python 3.13

# changelog.d/124.fixed
Fixed memory leak in long-running processes (#124)

Build changelog:

towncrier build --version 1.2.0
# Compiles fragments into CHANGELOG.md and deletes them

Version Constraints in Dependencies

When depending on other packages, choose constraints carefully:

Constraint Types

SpecifierMeaningExample
==Exact version==1.2.3
>=Minimum version>=1.2.0
<=Maximum version<=2.0.0
!=Exclude version!=1.2.4
~=Compatible release~=1.2.0 means >=1.2.0,<1.3.0
>=,<Range>=1.2.0,<2.0.0

Recommendations by Project Type

# For Libraries (be permissive)
[project]
dependencies = [
    "requests>=2.28",      # Minimum only
    "click>=8.0,<9.0",     # Range with major cap
]

# For Applications (can be stricter)
[project]
dependencies = [
    "requests>=2.28.0,<3.0.0",
    "click>=8.1.0,<9.0.0",
]

# For Reproducible Builds (pin everything)
# Use a lock file instead (poetry.lock, requirements.txt)

The Compatible Release Operator (~=)

~=1.2.3  # Equivalent to >=1.2.3,<1.3.0
~=1.2    # Equivalent to >=1.2,<2.0
~=1      # Equivalent to >=1,<2

Use for dependencies that follow SemVer properly.

Choosing a Versioning Strategy

Decision Tree

Is your project a reusable library?
├── Yes → Use SemVer (1.2.3)
│         Users depend on your API stability
│
└── No → Is backward compatibility meaningful?
         ├── Yes → Use SemVer
         │
         └── No → Consider CalVer (2026.01)
                  Good for applications, tools

Common Patterns by Project Type

Project TypeRecommended SchemeExample
Library (PyPI)SemVerrequests 2.31.0
CLI ToolSemVer or CalVerblack 23.7.0
Web AppCalVer2026.01.15
FrameworkSemVerdjango 5.0.1
Data PipelineCalVer2026.1

Best Practices

1. Start at 0.1.0

Don't start at 1.0.0 unless your API is stable:

0.1.0  # Initial development
0.2.0  # Add features freely
0.9.0  # Getting close to stable
1.0.0  # API is now stable

2. Document Breaking Changes

## [2.0.0] - 2026-01-15

### BREAKING CHANGES

- Removed deprecated `old_function()`. Use `new_function()` instead.
- Changed `process()` to require `strict` parameter.
- Minimum Python version is now 3.10.

### Migration Guide

See [MIGRATION.md](MIGRATION.md) for upgrade instructions.

3. Use Pre-Releases for Testing

# Release alpha for early feedback
bump2version --new-version 2.0.0a1 major

# After testing, release beta
bump2version --new-version 2.0.0b1 major

# Release candidate
bump2version --new-version 2.0.0rc1 major

# Final release
bump2version --new-version 2.0.0 major

4. Never Reuse Version Numbers

Even if you yank a release, that version is forever taken:

1.2.0  # Released with bug
1.2.0  # Cannot re-upload fixed version!
1.2.1  # Must use new version for fix

5. Automate Version Management

Use CI/CD to enforce versioning:

# .github/workflows/release.yml
- name: Check version matches tag
  run: |
    TAG_VERSION=${GITHUB_REF#refs/tags/v}
    PKG_VERSION=$(python -c "import my_package; print(my_package.__version__)")
    if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
      echo "Version mismatch: tag=$TAG_VERSION, package=$PKG_VERSION"
      exit 1
    fi

Version in pyproject.toml

Static Version

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

Dynamic Version

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

# With setuptools-scm
[tool.setuptools_scm]

# Or read from __init__.py
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}

Common Versioning Mistakes

Mistake 1: Inconsistent Version Locations

# pyproject.toml says 1.2.3
# __init__.py says 1.2.2
# Bad! Use dynamic versioning or bump tools

Mistake 2: Breaking Changes in Minor Version

# 1.2.0
def process(data):
    return result

# 1.3.0 - WRONG! This is a breaking change
def process(data, required_param):  # New required param
    return result

# Correct: This should be 2.0.0

Mistake 3: Not Using Pre-Releases

1.0.0
2.0.0  # Big breaking release, users surprised

# Better approach:
1.0.0
2.0.0a1  # Users can test
2.0.0b1  # More testing
2.0.0rc1 # Almost ready
2.0.0    # Users are prepared

Next Steps

For more Python development guides, explore our complete Python packaging series.

Frequently Asked Questions

Find answers to common questions

Semantic versioning (SemVer) uses MAJOR.MINOR.PATCH format where MAJOR changes break compatibility, MINOR adds features backward-compatibly, and PATCH fixes bugs. Use it for libraries where users depend on API stability. It's the most widely understood versioning scheme.

Need Expert IT & Security Guidance?

Our team is ready to help protect and optimize your business technology infrastructure.