uv Build and Publish Checklist: When the uv Backend Is Enough, and When It Is Not
uv can simplify Python package builds and publishing, but it does not remove the need to prove that your wheel or source distribution works outside your local workspace. This checklist helps maintainers decide when the uv build backend is enough, when another backend is required, and how to verify artifacts before publishing.
uv build can make Python package release work easier, but it does not make the produced wheel or source distribution correct by default. A project can resolve its lockfile, pass tests from the checkout, and start inside Docker, then still fail for someone installing the published artifact.
That failure usually comes from a plain gap: a local file, path dependency, package-data directory, or console-script import existed in the workspace but not in the distribution. This checklist is for maintainers who want to use uv without confusing a working repository with a releasable package. The commands below are proposed checks for your project. They were not executed in this workflow, and no command output is invented.
The rule: test the artifact, not the checkout
uv gives maintainers one tool surface for project management, distribution builds, and publishing. The official package guide documents uv build and uv publish, and the backend guide documents uv_build as a build backend. Those tools are helpful. They still leave you with one hard question: can an outside consumer install and use the thing you plan to publish?
A consumer receives metadata and files from a wheel, an sdist, or a package index. They do not receive your editable checkout, workspace source configuration, private path dependency, Docker layer cache, or local data folder unless those pieces are part of the artifact or reachable from declared metadata. That is why the artifact deserves its own test lane.
This is the same evidence mindset behind TensorRT engine build observability. A progress message is not the same as proof. In packaging, a green local test run is not the same as a consumer install from a wheel.
When uv_build is enough, and when it is not
The uv build backend is a good candidate when the package is pure Python and conventional: normal modules, a clear src layout, straightforward metadata, simple package data, and no backend-specific release hooks. A small library or CLI with a narrow public API is a better first migration than a package that also compiles native code or assembles generated artifacts during release.
The current boundary matters: uv_build currently only supports pure Python. If the package itself builds extension modules, another backend is required. That is the documented limit. uv can still be valuable as the frontend around another backend, but backend replacement should wait until artifact parity is proven. Python GPU and native-adjacent projects, like the workloads discussed in the NVIDIA Warp route acceptance test, need that split kept clear. Packaging a Python wrapper is not the same job as compiling every dependency it can call.
| Project shape | Backend decision | What to prove before release |
|---|---|---|
Pure-Python library with src layout | uv_build is a reasonable candidate | Wheel imports, sdist rebuilds, package data is present |
| Simple CLI package | uv_build may fit | Installed console script calls the packaged module |
| Package with generated files | Decide after generation path is clear | Generated outputs are in both artifacts or rebuilt from sdist |
| Package with extension modules | Use another backend | Native build steps, platform tags, and toolchains are handled |
| Mature custom setuptools or Hatchling project | Keep current backend first | uv frontend works without losing existing release behavior |
Worked example: invoice-normalizer
Use a tiny hypothetical package so the checks have something concrete to inspect. The package normalizes invoice dictionaries and ships a currency map as package data. It also has a console script, which gives you one more place for packaging mistakes to show up.
pyproject.toml:
[build-system]
requires = ["uv_build>=0.12.22,<0.13"]
build-backend = "uv_build"
[project]
name = "invoice-normalizer"
version = "0.1.0"
description = "Normalize simple invoice payloads."
readme = "README.md"
requires-python = ">=3.11"
dependencies = []
[project.scripts]
invoice-normalizer = "invoice_normalizer:main"README.md is not optional in this example, because pyproject.toml points at it. Create it before building:
# invoice-normalizer
Normalize simple invoice payloads for packaging checks.src/invoice_normalizer/__init__.py:
from importlib.resources import files
import json
def normalize_invoice(payload: dict) -> dict:
currency = payload.get("currency", "USD").upper()
data = json.loads(files(__package__).joinpath("data/currencies.json").read_text())
if currency not in data["supported"]:
raise ValueError(f"unsupported currency: {currency}")
return {"invoice_id": str(payload["invoice_id"]), "currency": currency}
def main() -> None:
print(normalize_invoice({"invoice_id": 123, "currency": "usd"}))src/invoice_normalizer/data/currencies.json:
{"supported": ["USD", "EUR", "GBP"]}This is not a production invoice system. It is deliberately small. It has a public import, a console script, and package data, which are exactly the parts that often work from the checkout while failing from the built wheel.
Build and inspect by exact archive name
First build both distributions:
uv buildThen inspect the files you expect by exact name. Avoid vague wildcard examples in documentation and CI, because stale artifacts can sit in dist and make a check look better than it is.
python -m zipfile --list dist/invoice_normalizer-0.1.0-py3-none-any.whl
python -m tarfile --list dist/invoice_normalizer-0.1.0.tar.gzLook for invoice_normalizer/__init__.py, invoice_normalizer/data/currencies.json, project metadata, README inputs if used, license files if declared, and console-script entry point metadata. The expected signal is that the artifact contains the same files your public API needs after installation. That is not recorded output from this workflow.
| Check | Command or review target | Expected signal |
|---|---|---|
| Wheel contents | python -m zipfile --list ...whl | Module and data file appear in the wheel |
| sdist contents | python -m tarfile --list ...tar.gz | Source, metadata inputs, and data file appear in the sdist |
| Clean wheel install | Install exact wheel outside checkout | Import path comes from installed package, not repository |
| Package data load | Call normalize_invoice after install | JSON data is available through importlib.resources |
| CLI smoke test | Run installed invoice-normalizer | Entry point resolves to packaged code |
Rebuild from the sdist
If you publish an sdist, prove that it can rebuild the wheel from the files it contains. This catches a different class of error than inspecting the first wheel. A missing README, generated file, or data directory may be invisible until a consumer builds from source.
REPO_DIR="$PWD"
SDIST_PATH="$REPO_DIR/dist/invoice_normalizer-0.1.0.tar.gz"
SDIST_CHECK_DIR="$(mktemp -d)"
cd "$SDIST_CHECK_DIR"
python -m tarfile --extract "$SDIST_PATH"
cd invoice_normalizer-0.1.0
uv build --wheel
python -m zipfile --list dist/invoice_normalizer-0.1.0-py3-none-any.whlThe expected signal is a rebuilt wheel created from the sdist checkout, followed by archive contents that still include the module and package data. Treat this as a proposed validation sequence, not as a report that the commands were run here.
Install the wheel outside the checkout
For clean install checks, create the environment outside the repository, install the exact wheel through an absolute path, then run import and package-data checks there. Do not count uv run from the source checkout as packaging proof.
REPO_DIR="$PWD"
WHEEL_PATH="$REPO_DIR/dist/invoice_normalizer-0.1.0-py3-none-any.whl"
WHEEL_CHECK_DIR="$(mktemp -d)"
cd "$WHEEL_CHECK_DIR"
uv venv .venv
. .venv/bin/activate
uv pip install "$WHEEL_PATH"
python -c 'from invoice_normalizer import normalize_invoice; assert normalize_invoice({"invoice_id": 7, "currency": "eur"}) == {"invoice_id": "7", "currency": "EUR"}'
python -c 'from importlib.resources import files; assert files("invoice_normalizer").joinpath("data/currencies.json").is_file()'
invoice-normalizerThe import assertion checks the public function. The resources assertion checks that the JSON file is packaged, not merely present in the source tree. The CLI command checks that entry point metadata resolves after installation. Again, those are expected signals for a maintainer to record in CI or release notes; this article is not claiming local execution.
What uv build --no-sources proves
uv build --no-sources is worth using, but its boundary needs to stay exact. It disables tool.uv.sources for build dependency resolution from build-system.requires. That can expose source-configuration assumptions around build requirements.
It does not itself resolve runtime dependencies. It does not audit runtime imports. It does not reveal every undeclared package your code imports after installation. If a module imports requests at runtime but forgets to declare it, --no-sources is not the check that proves the mistake. A clean install followed by import and workflow smoke tests is the better place to catch that.
Use --no-sources as one check in a release sequence, not as the release gate. If it fails, decide what the package is supposed to be. Public packages need versioned, accessible requirements. Private packages need documented index access. Internal-only workspace packages should not be published as if outside consumers can install them. Historical GitHub discussion around uv backend separation is useful practitioner context, but issue 3957 should not be treated as proof of a current source-configuration bug.
{
"checklist": "Outside-Consumer Build Check",
"minimumSignals": ["wheel built", "sdist built", "archives inspected by exact name", "wheel installed outside checkout", "import and package data verified"],
"noSourcesBoundary": "Checks build dependency resolution without tool.uv.sources; it is not a runtime dependency audit."
}Docker and publishing checks
Docker helps when you want a pinned uv version, repeatable dependency sync, and a clean CI environment. It can also hide packaging errors if the image copies the source tree directly or reuses cached layers. Keep application-image validation separate from distribution validation. A container that starts is not proof that the wheel includes currencies.json.
Before uv publish, confirm the package name, version, Python requirement, dependencies, README inputs, license metadata, and target index configuration. After publishing, verify as a consumer from the target index by exact version. Upload success only proves the registry accepted files. It does not prove users can import the package, run the CLI, or load packaged data.
PUBLISH_CHECK_DIR="$(mktemp -d)"
TARGET_INDEX_URL="https://test.pypi.org/simple"
cd "$PUBLISH_CHECK_DIR"
uv venv .venv
. .venv/bin/activate
uv pip install --index-url "$TARGET_INDEX_URL" "invoice-normalizer==0.1.0"
python -c 'from invoice_normalizer import normalize_invoice; assert normalize_invoice({"invoice_id": 42}) == {"invoice_id": "42", "currency": "USD"}'
python -c 'from importlib.resources import files; assert files("invoice_normalizer").joinpath("data/currencies.json").is_file()'
invoice-normalizerFor a real release, point TARGET_INDEX_URL at the index you actually published to, and add any required auth or extra-index configuration explicitly. The expected signal is a clean install by exact version from that index, followed by the same import, data, and CLI checks used before publishing.
Common mistakes
The first mistake is treating lock success as publish success. A lockfile is project-context evidence, not distribution evidence. Another common mistake is switching backend, dependency layout, CI, and publishing credentials in one change. That makes failures harder to read. Change one layer at a time.
A third mistake is skipping the sdist because the wheel installed locally. Some consumers still build from source, and a broken sdist can become the first failure they see. A fourth is assuming Docker answered the artifact question. It did not, unless the container installed the same wheel or sdist a consumer will receive.
The better adoption path is almost dull, which is a compliment in release engineering: use uv as the frontend first, keep the current backend if it already encodes release behavior, and adopt uv_build only when the project shape fits. Treat the wheel and sdist as the product. Validate them from outside the checkout. Choose the backend after the artifact evidence supports it.
Key Takeaways
- 1uv simplifies Python package build and publish workflows, but artifacts still need outside-consumer validation.
- 2uv_build currently only supports pure-Python projects; extension modules require another backend.
- 3uv build --no-sources checks build dependency resolution without tool.uv.sources, not runtime dependency completeness.
- 4Clean installation from built artifacts outside the checkout is the strongest local pre-publish signal.
- 5Docker repeatability does not prove wheel or sdist correctness.
Conclusion
uv is at its best when it makes packaging easier to reason about, not when it hides release assumptions behind a faster command. Treat the wheel and sdist as the product, validate them from outside the checkout, and choose uv_build only when the package is pure Python and the built artifacts prove the release story.
Frequently Asked Questions
What does uv build create?
uv build creates Python distribution artifacts such as wheels and source distributions, depending on the project and command used.
When should I use the uv build backend?
Use uv_build for compatible pure-Python packages with conventional layouts. Use another backend when the package builds extension modules or needs custom backend behavior.
What is uv build --no-sources useful for?
It disables tool.uv.sources for build dependency resolution from build-system.requires, helping expose build-source assumptions. It is not a runtime dependency audit.
Does a working Docker image prove my Python package is correct?
No. Docker can copy source files or use cached layers that hide missing wheel or sdist contents.
What should I verify after publishing?
Install the published version from the target index in a clean environment, then verify imports, console scripts, package data, and a minimal consumer workflow.
Sources
Written by
Hamza DiazHamza Diaz is the founder of Optijara, where he builds practical AI agents, automation systems, and Copilot workflows for service businesses. He writes about AI operations, agent strategy, and real-world implementation for teams that want usable systems instead of hype.
