Building and Releasing

Updated

September 8, 2026

Building and Releasing

A release is a version in pyproject.toml, a wheel (and usually an sdist) from uv build, and a tag that matches. The code that runs after install should print that same version. Do not have three version strings.

Mental model

Version is [project].version (or a dynamic scheme you chose on purpose). Bump it when you publish, not when you save a file.

uv build asks the build backend (hatchling here) for two artifacts:

  • sdist — source tree in a tarball (deskshift-1.2.0.tar.gz)
  • wheel — installable zip (deskshift-1.2.0-py3-none-any.whl)

Wheels are what uv pip install / uv add consume from an index. The py3-none-any tags mean “pure Python, any OS.” That is the boring target until you wrap a C library.

Releasing is: tests green, ruff clean, version bumped, uv build, upload to your index, git tag v1.2.0. This chapter shows the files and the build command, not a hosting vendor.

Worked examples

Case 1: Version lives in pyproject.toml

Save as a project:

pyproject.toml:

[project]
name = "deskshift"
version = "1.2.0"
description = "Shift labels for a small desk."
requires-python = ">=3.14"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

deskshift/__init__.py:

# deskshift/__init__.py
__version__ = "1.2.0"


def shift_label(name: str) -> str:
    return f"shift {name}"

The duplicated "1.2.0" is the thing you will eventually replace with importlib.metadata.version("deskshift") after the package is installed. Until then, one constant next to the function is acceptable if you bump both in the same commit.

Case 2: A complete module that prints the version

Save as show_version.py and run it without installing. This is the same string you put in pyproject.toml.

# show_version.py
__version__ = "1.2.0"


def shift_label(name: str) -> str:
    return f"shift {name}"


def main() -> None:
    print(__version__)
    print(shift_label("night"))


if __name__ == "__main__":
    main()

Run:

uv run python show_version.py

Output:

1.2.0
shift night

Case 3: Build commands

From the project directory that contains pyproject.toml:

uv build

You should see files under dist/:

dist/deskshift-1.2.0.tar.gz
dist/deskshift-1.2.0-py3-none-any.whl

Install the wheel into an isolated check:

uv pip install dist/deskshift-1.2.0-py3-none-any.whl
uv run python -c "import deskshift; print(deskshift.__version__)"

Expected output of that -c:

1.2.0

If the wheel name differs, list dist/ and install the file that is there. Do not edit a wheel by hand.

Case 4: Read the installed version

Save as installed_version.py. importlib.metadata.version looks at the installed distribution. In this listing we fall back to a constant so the file still runs when you have not installed deskshift.

# installed_version.py
from importlib.metadata import PackageNotFoundError, version

__version__ = "1.2.0"


def package_version() -> str:
    try:
        return version("deskshift")
    except PackageNotFoundError:
        return __version__


def main() -> None:
    print(package_version())


if __name__ == "__main__":
    main()

Run:

uv run python installed_version.py

Output:

1.2.0

After a real install, the metadata path wins. That is the boring long-term source of truth.

The trap

A comment, a constant, and pyproject.toml that disagree.

Save as wrong_version.py:

# wrong_version.py
# version 1.1.0  — leftover comment
__version__ = "1.2.0"


def main() -> None:
    print("package 1.0.0")
    print(__version__)


if __name__ == "__main__":
    main()

Run:

uv run python wrong_version.py

Output:

package 1.0.0
1.2.0

Three numbers, one process. Support will ask “which version?” and every answer will be true. Print __version__ (or importlib.metadata.version) only. Delete the comment. Bump pyproject.toml in the same change.

The boring rule

  • One version per release, in pyproject.toml, tagged in git as vX.Y.Z.
  • uv build produces sdist + wheel. Ship both unless you have a reason not to.
  • Pure Python wheels (py3-none-any) until you truly need compiled code.
  • After install, prefer importlib.metadata.version("deskshift").
  • Do not release from a dirty tree. Tests and ruff check first.

Try this

  1. Change __version__ in show_version.py to 1.2.1 and print shift_label("day").
  2. Add authors = [{name = "Desk"}] to the Case 1 pyproject.toml.
  3. In installed_version.py, print a prefix deskshift before the version.