Version Source
March 22, 2026 ยท View on GitHub
uv-dynamic-versioning version source allows you to set a version based on VCS.
Add tool.hatch.version & build-system in your pyproject.toml and configure them to use uv-dynamic-versioning.
[build-system]
requires = ["hatchling", "uv-dynamic-versioning"]
build-backend = "hatchling.build"
Also remove version in project and set it in project.dynamic (dynamic = ["version"]).
Before
[project]
name = "..."
version = "0.1.0"
After
[project]
name = "..."
dynamic = ["version"]
Then this plugin works out of the box (defaults to using the semver style).
For example:
$ git tag v1.0.0
$ uv build
Building source distribution...
Building wheel from source distribution...
Successfully built dist/foo-1.0.0.tar.gz
Successfully built dist/foo-1.0.0-py3-none-any.whl
# check METADATA file (ref. https://packaging.python.org/en/latest/specifications/core-metadata/)
$ tar -xf dist/foo-1.0.0-py3-none-any.whl
$ head foo-1.0.0.dist-info/METADATA
Metadata-Version: 2.4
Name: foo
Version: 1.0.0
Note
You can use uv-dynamic-versioning command to check the version to be used:
$ uvx uv-dynamic-versioning
1.0.0
Configuration
Note
- Configuration is almost same as poetry-dynamic-versioning. But
format-jinja-importsandfix-shallow-repositoryare not supported. - The following descriptions are excerpts from poetry-dynamic-versioning.
You may configure the following options under [tool.uv-dynamic-versioning]:
-
vcs(string, default:any): This is the version control system to check for a version. One of:any,git,mercurial,darcs,bazaar,subversion,fossil,pijul. -
metadata(boolean, default: unset): If true, include the commit hash in the version, and also include a dirty flag ifdirtyis true. If unset, metadata will only be included if you are on a commit without a version tag. This is ignored whenformatorformat-jinjais used. -
tagged-metadata(boolean, default: false): If true, include any tagged metadata discovered as the first part of the metadata segment. Has no effect whenmetadatais set to false. This is ignored whenformatorformat-jinjais used. -
dirty(boolean, default: false): If true, include a dirty flag in the metadata, indicating whether there are any uncommitted changes. Has no effect whenmetadatais set to false. This is ignored whenformatorformat-jinjais used. -
pattern(string): This is a regular expression which will be used to find a tag representing a version. When this is unset, Dunamai's default pattern is used, which requires thevprefix (for example,v1.0.0).There must be a capture group named
basewith the main part of the version. Optionally, it may contain another two groups namedstageandrevisionfor prereleases, and it may contain a group namedtagged_metadatato be used with thetagged-metadataoption. There may also be a group namedepochfor the PEP 440 concept.If the
basegroup is not included, then this will be interpreted as a named preset from the DunamaiPatternclass. This includes:default,default-unprefixed(makes thevprefix optional). You can check the default for your installed version of Dunamai by running this command:uv run python -c "import dunamai; print(dunamai.Pattern.Default.regex())"Remember that backslashes must be escaped in the TOML file.
# Regular expression: pattern = '(?P<base>\d+\.\d+\.\d+)' # Named preset: pattern = "default-unprefixed" -
pattern-prefix(string): This will be inserted after the pattern's start anchor (^). For example, to match tags likesome-package-v1.2.3, you can keep the default pattern and set the prefix tosome-package-. -
format(string, default: unset): This defines a custom output format for the version. Available substitutions:{base}{stage}{revision}{distance}{commit}{dirty}{tagged_metadata}{branch}{branch_escaped}which omits any non-letter/number characters{timestamp}of the current commit, which expands to YYYYmmddHHMMSS as UTC
Example:
v{base}+{distance}.{commit} -
format-jinja(string, default: unset): This defines a custom output format for the version, using a Jinja template. When this is set,formatis ignored.Available variables:
version(dunamai.version)base(string)stage(string or None)revision(integer or None)distance(integer)commit(string)dirty(boolean)tagged_metadata(string or None)version(dunumai.Version)env(dictionary of environment variables)branch(string or None)branch_escaped(string or None)timestamp(string or None)major(integer)minor(integer)patch(integer)
Available functions:
bump_version(from Dunamai)serialize_pep440(from Dunamai)serialize_semver(from Dunamai)serialize_pvp(from Dunamai)
Simple example:
format-jinja = "{% if distance == 0 %}{{ base }}{% else %}{{ base }}+{{ distance }}.{{ commit }}{% endif %}"Complex example:
format-jinja = """ {%- if distance == 0 -%} {{ serialize_pep440(base, stage, revision) }} {%- elif revision is not none -%} {{ serialize_pep440(base, stage, revision + 1, dev=distance, metadata=[commit]) }} {%- else -%} {{ serialize_pep440(bump_version(base), stage, revision, dev=distance, metadata=[commit]) }} {%- endif -%} """ -
format-jinja-imports(array of tables, default: empty): This defines additional things to import and make available to theformat-jinjatemplate. Each table must contain amodulekey and may also contain anitemkey. Consider this example:format-jinja-imports = [ { module = "foo" }, { module = "bar", item = "baz" }, ]This is roughly equivalent to:
import foo from bar import bazfooandbazwould then become available in the Jinja formatting. -
style(string, default: unset): One of:pep440,semver,pvp. These are pre-configured output formats. If you set both astyleand aformat, then the format will be validated against the style's rules. Ifstyleis unset, the default output format will follow PEP 440, but a customformatwill only be validated ifstyleis set explicitly.Regardless of the style you choose, the dynamic version is ultimately subject to Hatchling's validation as well, and Hatchling is designed around PEP 440 versions. Hatchling can usually understand SemVer/etc input, but sometimes, Hatchling may reject an otherwise valid version format.
-
latest-tag(boolean, default: false): If true, then only check the latest tag for a version, rather than looking through all the tags until a suitable one is found to match thepattern. -
highest-tag(boolean, default: false): If true, then select the numerically highest version, even if it is not the chronologically latest tag. -
bump(boolean or table, default: false): If enabled, then increment the last part of the versionbaseby 1, unless thestageis set, in which case increment therevisionby 1 or set it to a default of 2 if there was norevision. Does nothing when on a commit with a version tag. One of:- When set to a boolean, true means enable bumping, with other settings as default.
- When set to a table, these fields are allowed:
enable(boolean, default: false): If true, enable bumping.index(integer, default: -1): Numerical position to increment in the base. This follows Python indexing rules, so positive numbers start from the left side and count up from 0, while negative numbers start from the right side and count down from -1.
Example, if there have been 3 commits since the
v1.3.1tag:- PEP 440 with
bump = false:1.3.1.post3.dev0+28c1684 - PEP 440 with
bump = true:1.3.2.dev3+28c1684
-
tag-branch(string, default: unset): Branch on which to find tags, if different than the current branch. This is only used for Git currently. -
full-commit(boolean, default: false): If true, get the full commit hash instead of the short form. This is only used for Git and Mercurial. -
strict(boolean, default: false): If true, then fail instead of falling back to 0.0.0 when there are no tags. -
ignore-untracked(boolean, default: false): If true, ignore untracked files when determining whether the repository is dirty. -
commit-length(integer, default: unset): Use this many characters from the start of the full commit hash. -
commit-prefix(string, default: unset): Add this prefix to the commit ID when serializing. This can be helpful when an all-numeric commit would be misinterpreted. For example, "g" is a common prefix for Git commits. -
escape-with(string, default: unset): When escaping, replace invalid characters with this substitution. The default is simply to remove invalid characters. -
fallback-version(str, default: unset): Version to be used if an error occurs when obtaining the version, for example, there is no.git/. If not specified, unsuccessful version obtaining from vcs will raise an error. -
from-file: This section lets you read the version from a file instead of the VCS.source(string): If set, read the version from this file. It must be a path relative to the location of pyproject.toml. By default, the plugin will read the entire content of the file, without leading and trailing whitespace.pattern(string): If set, use this regular expression to extract the version from the file. The first capture group must contain the version.
Examples
Default (no tool.uv-dynamic-versioning in pyproject.toml):
$ git tag v1.0.0
$ uv build
Building source distribution...
Building wheel from source distribution...
Successfully built dist/foo-1.0.0.tar.gz
Successfully built dist/foo-1.0.0-py3-none-any.whl
With pattern:
[tool.uv-dynamic-versioning]
pattern = "default-unprefixed"
$ git tag 1.0.0
$ uv build
Building source distribution...
Building wheel from source distribution...
Successfully built dist/foo-1.0.0.tar.gz
Successfully built dist/foo-1.0.0-py3-none-any.whl
Environment Variables
In addition to the project-specific configuration above, you can apply some global overrides via environment variables.
UV_DYNAMIC_VERSIONING_BYPASS: Use this to bypass the VCS mechanisms and use a static version instead. The value of the environment variable will be used as the version for the active project and any path/SSH dependencies that also use the plugin. This is mainly for distro package maintainers who need to patch existing releases, without needing access to the original repository.
__version__ Attribute
You may want to set __version__ attribute in your library. There are two ways for that. Using importlib.metadata and using version build hook.
importlib.metadata
Note
This is very handy, but it's known that importlib.metadata is relatively slow.
Don't use this method when performance is critical.
# __init__.py
import importlib.metadata
__version__ = importlib.metadata.version(__name__)
This trick may fail if a package is installed in development mode. Setting a fallback for importlib.metadata.PackageNotFoundError may be a good workaround.
import importlib.metadata
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0"
Version Build Hook
You can write a version to a file when you run a build by using Hatch's official version build hook.
For example:
[tool.hatch.build.hooks.version]
path = "path/to/_version.py"
template = '''
version = "{version}"
'''
Note
A version file should not be included in VCS. It's better to ignore it in .gitignore.
.gitignore
path/to/_version.py