Versioning & Changelogs
Semantic versioning communicates breaking changes to consumers. Changelogs document what changed between releases.
Recipe
[project]
version = "1.2.3" # MAJOR.MINOR.PATCH# CHANGELOG.md
## [1.2.3] - 2026-07-09
### Fixed
- Handle empty invoice list in exportWhen to reach for this:
- Publishing libraries consumed by other teams
- Communicating breaking changes clearly
- Automating releases from git tags
Working Example
# Dynamic version from git tags (hatchling)
[project]
name = "myapp"
dynamic = ["version"]
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.hooks.vcs]
version-file = "src/myapp/_version.py"# CHANGELOG.md (Keep a Changelog format)
# Changelog
## [Unreleased]
### Added
- Batch invoice export endpoint
## [1.2.0] - 2026-06-15
### Changed
- **BREAKING:** `Invoice.amount` now returns Decimal, not float
### Migration
- Replace `float(invoice.amount)` with `invoice.amount` (already Decimal)git tag v1.2.0
uv build # version read from tagWhat this demonstrates:
- SemVer: breaking = MAJOR, feature = MINOR, fix = PATCH
- Dynamic versioning reads git tags automatically
- Changelog follows Keep a Changelog format
- Breaking changes include migration notes
Deep Dive
SemVer Rules
| Change | Bump | Example |
|---|---|---|
| Breaking API change | MAJOR | 1.0.0 -> 2.0.0 |
| New feature, backward compatible | MINOR | 1.0.0 -> 1.1.0 |
| Bug fix | PATCH | 1.0.0 -> 1.0.1 |
| Pre-release | suffix | 1.0.0b1, 1.0.0rc1 |
Gotchas
- Breaking change in MINOR - breaks consumer trust. Fix: bump MAJOR for any breaking API change.
- No changelog entry - consumers cannot assess upgrade risk. Fix: update CHANGELOG.md with every release.
- Version hardcoded in multiple files - drift. Fix: single source in pyproject.toml or dynamic from VCS.
- 0.x semver confusion - 0.y.z can break anytime. Fix: document stability policy; move to 1.0 when API is stable.
- Publishing same version twice - PyPI rejects. Fix: bump version before every publish.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| CalVer (2026.07.1) | Date-driven releases | Library with semver expectations |
| git tags only | Internal tools | Public libraries |
| semantic-release (bot) | Automated changelog | Manual release process preferred |
FAQs
When is 1.0.0?
When the public API is stable and you commit to semver semantics.
How do I automate versions?
hatch-vcs, setuptools-scm, or semantic-release from conventional commits.
CHANGELOG or GitHub Releases?
Both. CHANGELOG in repo; GitHub Releases for distribution and notifications.
What is a breaking change?
Removing public API, changing function signatures, or altering behavior documented as stable.
How do I deprecate?
Warning in N release, remove in N+1 MAJOR. Document in changelog.
Pre-release versions?
1.0.0a1 (alpha), 1.0.0b1 (beta), 1.0.0rc1 (release candidate).
Who maintains the changelog?
The engineer merging the PR adds an entry under [Unreleased].
How do I version applications?
Applications can use CalVer or semver. Libraries must use semver.
Conventional commits?
feat:, fix:, BREAKING CHANGE: prefixes enable automated changelog generation.
How do I yank a bad release?
PyPI yank (not delete). Consumers already pinned are unaffected; new installs blocked.
Related
- Packaging Basics - version in metadata
- Publishing to PyPI - release workflow
- Packaging Best Practices - release policy
- Enterprise Delivery - release process
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.