# 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](https://pydevtools.com/handbook/reference/pyproject.toml.md). 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](https://github.com/ninoseki/uv-dynamic-versioning/), a [hatchling](https://pydevtools.com/handbook/reference/hatch.md) plugin that ships a sensible default configuration for [uv](https://pydevtools.com/handbook/reference/uv.md) projects.

## Prerequisites

- A Git repository for your Python project with at least one commit
- [uv](https://pydevtools.com/handbook/reference/uv.md) installed on your system
- A `src/your_package/` layout (the default for `uv init --package`)

## Configure the build system

Update `pyproject.toml` to use uv-dynamic-versioning as a [build backend](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md) dependency:

```toml
[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:

```toml
[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):

```console
$ git tag v0.1.0
```

## Build and verify

```console
$ 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](https://peps.python.org/pep-0440/) post-release and dev segment plus the commit hash as a local identifier:

```
your_project-0.1.0.post1.dev0+g6aefd32-py3-none-any.whl
```

These 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.

```yaml
- uses: actions/checkout@v5
  with:
    fetch-depth: 0
    fetch-tags: true
```

Dependabot and other shallow clones you cannot control: define a fallback so the build still succeeds when tags are missing.

```toml
[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:

```python
# 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 mode
```

This 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:

```console
$ git tag v0.2.0
$ git push origin v0.2.0
```

Then trigger a publish. Pair this with [trusted publishing to PyPI](https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing.md) 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](https://pydevtools.com/handbook/explanation/why-does-uv-use-hatch-as-a-backend.md), 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](https://setuptools-scm.readthedocs.io/) targets setuptools, [hatch-vcs](https://github.com/ofek/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:

```toml
[tool.uv-dynamic-versioning]
pattern = "default-unprefixed"
```

See the [uv-dynamic-versioning documentation](https://github.com/ninoseki/uv-dynamic-versioning/) 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: A Complete Guide](https://pydevtools.com/handbook/explanation/uv-complete-guide.md) covers what uv does, how fast it is, the core workflows, and recent releases.
- [uv-dynamic-versioning on GitHub](https://github.com/ninoseki/uv-dynamic-versioning/)
- [pyproject.toml reference](https://pydevtools.com/handbook/reference/pyproject.toml.md) covers project metadata fields including `dynamic`
- [What is a build backend?](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md) explains how hatchling and other backends work
- [Why does uv use hatch as a backend?](https://pydevtools.com/handbook/explanation/why-does-uv-use-hatch-as-a-backend.md) covers the default build backend for uv projects
- [How to publish to PyPI with trusted publishing](https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing.md) pairs well with tag-driven releases
- [uv reference](https://pydevtools.com/handbook/reference/uv.md) documents the `uv build` and `uv publish` commands
