How to add dynamic versioning to uv projects
Dynamic versioning generates version numbers from Git tags instead of requiring manual updates to a static version string in pyproject.toml. The release workflow becomes a single git tag command: the build backend reads the tag, and the wheel and source distribution filenames pick up the matching version automatically.
This guide uses uv-dynamic-versioning, a hatchling plugin that ships a sensible default configuration for uv projects.
Prerequisites
- A Git repository for your Python project with at least one commit
- uv installed on your system
- A
src/your_package/layout (the default foruv init --package)
Configure the build system
Update pyproject.toml to use uv-dynamic-versioning as a build backend dependency:
[build-system]
requires = ["hatchling", "uv-dynamic-versioning"]
build-backend = "hatchling.build"Set the version source
Mark the version field as dynamic and point hatchling at uv-dynamic-versioning:
[project]
name = "your-project"
dynamic = ["version"] # Remove any static version = "..." line
[tool.hatch.version]
source = "uv-dynamic-versioning"Create a Git tag
Tag a commit following the default pattern (a v prefix followed by a semantic version):
$ git tag v0.1.0
Build and verify
$ uv build
The built distribution’s filename includes the version derived from the tag, for example your_project-0.1.0-py3-none-any.whl.
Understand versions between tags
When the working tree is tagged exactly, the version is clean (0.1.0). When you build from a commit past the most recent tag, uv-dynamic-versioning appends a PEP 440 post-release and dev segment plus the commit hash as a local identifier:
your_project-0.1.0.post1.dev0+g6aefd32-py3-none-any.whlThese development versions sort higher than the last release but lower than the next tagged release, so uv pip install . on a feature branch installs a version that supersedes 0.1.0 without claiming to be 0.2.0. PyPI rejects local version identifiers (anything after +), so only clean tagged builds can be uploaded.
Fetch full history in CI
CI runners and release automation default to shallow clones, which strips the Git tags and history that uv-dynamic-versioning needs. Two common fixes:
GitHub Actions: tell actions/checkout to fetch the full history and tags.
- uses: actions/checkout@v5
with:
fetch-depth: 0
fetch-tags: trueDependabot and other shallow clones you cannot control: define a fallback so the build still succeeds when tags are missing.
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"Use the fallback as a safety net, not as a substitute for a full checkout. Published releases should always build from a real tag.
Expose the version at runtime
To make the version accessible within the package:
# src/your_package/__init__.py
import importlib.metadata
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development modeThis reads the version from installed package metadata, so it stays in sync with the Git tag without duplicating the value. The PackageNotFoundError branch only fires when the package is imported from a source tree that was never installed, for example when running tests directly against src/.
Release a new version
With dynamic versioning in place, cutting a release is two commands:
$ git tag v0.2.0
$ git push origin v0.2.0
Then trigger a publish. Pair this with trusted publishing to PyPI so the same tag that sets the version also triggers the release workflow.
Frequently asked questions
Do I still need to edit pyproject.toml on every release?
No. The whole point of dynamic versioning is that version is no longer a static string. The only file that changes on a release is the Git tag.
Does this work with the uv_build backend?
Not directly. uv-dynamic-versioning is a hatchling plugin, so the build-backend must be hatchling.build. If you prefer to stay on uv’s default backend, there is no equivalent plugin yet; switch the backend to hatchling for dynamic versioning.
How is uv-dynamic-versioning different from setuptools-scm or hatch-vcs?
All three read the version from Git. setuptools-scm targets setuptools, hatch-vcs targets hatchling, and uv-dynamic-versioning is a lighter hatchling plugin with defaults tuned for uv projects. For a new uv project, uv-dynamic-versioning is the shortest path. Existing hatch-vcs setups work fine as-is.
Can I use a different tag pattern?
Yes. Override the pattern under [tool.uv-dynamic-versioning]. For example, to drop the v prefix:
[tool.uv-dynamic-versioning]
pattern = "default-unprefixed"See the uv-dynamic-versioning documentation for the full list of patterns and style options.
Why is my CI build failing with “could not find a tag”?
The runner did a shallow clone. Set fetch-depth: 0 and fetch-tags: true on actions/checkout, or configure a fallback-version for environments you cannot control.
Related
- uv-dynamic-versioning on GitHub
- pyproject.toml reference covers project metadata fields including
dynamic - What is a build backend? explains how hatchling and other backends work
- Why does uv use hatch as a backend? covers the default build backend for uv projects
- How to publish to PyPI with trusted publishing pairs well with tag-driven releases
- uv reference documents the
uv buildanduv publishcommands
This handbook is free, independent, and ad-free. If it saved you time, consider sponsoring it on GitHub.