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

## Does uv support dynamic versioning natively?

Not from Git tags. uv's built-in `uv version --bump patch` (also `minor` and `major`) edits the static `version` string in `pyproject.toml`, so you still commit a version bump on every release. uv's default `uv_build` backend requires that static field and rejects a `dynamic = ["version"]` project at build time with `missing field version`. Reading the version from Git tags needs a hatchling plugin, which the rest of this guide configures. Native `uv_build` support is tracked in [astral-sh/uv#14561](https://github.com/astral-sh/uv/issues/14561).

## 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"
```

> [!NOTE]
> This sets [hatchling](https://pydevtools.com/handbook/reference/hatch.md) as the [build backend](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md), not uv's default `uv_build`. uv-dynamic-versioning is a hatchling plugin, and its README states it "doesn't work with the uv build backend right now." Building a `dynamic = ["version"]` project with `uv_build` fails with `missing field version`, so switching the backend to hatchling is the supported path for tag-driven versioning. See [Why did uv originally use Hatch as a build backend?](https://pydevtools.com/handbook/explanation/why-does-uv-use-hatch-as-a-backend.md).

## 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+91cc190-py3-none-any.whl
```

The `post1` counts commits since the tag and `+91cc190` is the short commit hash. 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.

## Choose a version style and tag pattern

uv-dynamic-versioning computes versions with [dunamai](https://github.com/mtkennerly/dunamai), the engine shared by several VCS-versioning tools. Configure its output under `[tool.uv-dynamic-versioning]`:

```toml
[tool.uv-dynamic-versioning]
style = "pep440"        # also "semver" or "pvp"
pattern = "default"     # "default" requires a "v" prefix; "default-unprefixed" drops it
```

- `style` selects the version scheme: `pep440` (the default), `semver`, or `pvp`. The `pep440` default produces `0.1.0.post1.dev0+91cc190`. Wheel and sdist filenames are normalized to PEP 440 regardless, so keep `pep440` unless another tool reads the raw version string.
- `pattern` decides which tags count as releases. `default` matches `v1.2.3`; `default-unprefixed` matches `1.2.3`.
- The commit hash carries no prefix by default. Set `commit-prefix = "g"` for the `git describe`-style `+g91cc190`.

## 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/`.

## Automate tag-to-publish releases with GitHub Actions

Tag-driven versioning pairs with a workflow that triggers on the tag itself: push `v0.2.0`, and CI builds the distribution with that exact version and publishes it. Create `.github/workflows/release.yml`:

```yaml
name: Release

on:
  push:
    tags: ["v*"]

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # OIDC token for trusted publishing to PyPI
    steps:
      - uses: actions/checkout@v7.0.0
        with:
          fetch-depth: 0      # full history so the tag is present
          fetch-tags: true    # fetch tags even on a tag-triggered run
      - uses: astral-sh/setup-uv@v8.2.0
      - run: uv build
      - run: uv publish
```

The `fetch-depth: 0` and `fetch-tags: true` lines are the part specific to dynamic versioning. `actions/checkout` does a shallow clone by default, and uv-dynamic-versioning derives the version from Git history. Without the full history and tags, the build falls back to a `0.0.0`-based development version (for example `0.0.0.post1.dev0+91cc190`) instead of the `0.2.0` you tagged.

`uv publish` uploads via [trusted publishing](https://pydevtools.com/handbook/how-to/how-to-publish-to-pypi-with-trusted-publishing.md) when the job has `id-token: write`, so no PyPI API token lives in CI. Configure the PyPI side once by adding your repository and `release.yml` as a trusted publisher, which that guide walks through. To gate releases behind a manual GitHub Release instead of a raw tag push, swap the trigger for `on: release: types: [published]`.

With the workflow committed, cutting a release is two commands:

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

The pushed tag triggers the workflow, which builds `0.2.0` and publishes it.

## Handle shallow clones you cannot control

Dependabot and some CI setups do shallow clones that strip tags, and you cannot always add `fetch-depth: 0`. Define a fallback so the build still succeeds when the tag is missing:

```toml
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
```

Use the fallback as a safety net, not a substitute for a full checkout. Published releases should always build from a real tag.

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

### Should I use this or uv's `uv version` command?

They solve different problems. `uv version --bump patch` (also `major` and `minor`) edits the static `version` string in `pyproject.toml`, so the file stays the source of truth and the tag is created afterward. uv-dynamic-versioning inverts that: the Git tag is the version, and there is no string to bump or forget. Reach for `uv version` when you want a committed version field and a manual release step; reach for uv-dynamic-versioning when you want the tag to drive the version with no edits.

One trade-off comes with the switch: once `version` is dynamic, `uv version` stops working and reports `We cannot get or set dynamic project versions`. Reading dynamic versions through `uv version` is tracked in [astral-sh/uv#14137](https://github.com/astral-sh/uv/issues/14137).

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

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

### Why does `uv add` fail on a dynamic-versioning project?

It doesn't, as long as only `version` is dynamic. `uv add` builds the project to resolve it, and that build reads the tag like any other. The failure appears when `dependencies` is also marked dynamic (`dynamic = ["version", "dependencies"]`): `uv add` writes the new package into `[project.dependencies]`, which cannot coexist with a dynamic `dependencies` field, so the build aborts with `Metadata field dependencies cannot be both statically defined and listed in field project.dynamic`. Keep `dependencies` static and let uv manage it; only `version` needs to be dynamic. Reconciling the two is tracked in [astral-sh/uv#12837](https://github.com/astral-sh/uv/issues/12837).

## 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
- [Setting up GitHub Actions with uv](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md) covers the CI basics the release workflow builds on
- [uv reference](https://pydevtools.com/handbook/reference/uv.md) documents the `uv build` and `uv publish` commands
