Python's Build System with uv
The frontend/backend split, the wheel pipeline, and why uv sync builds your project.
Python packaging has a frontend/backend split that JavaScript lacks. When you uv sync, Python builds and installs your project, not just its dependencies. This post explains the model. I applied it in Adding Dagger CI to a Polyglot Monorepo.
The Problem
uv sync does more than install dependencies. It installs dependencies, then builds and installs the project itself. Most package managers stop at dependencies. Python’s does not. The difference matters for Docker layer caching, and it matters the first time a build error says “unable to determine which files to ship inside the wheel.”
What Changed
The key concepts are the frontend/backend split, the wheel pipeline, and editable installs.
The frontend/backend split
Python packaging separates two roles defined in PEP 517.
The installer frontend is what you type commands into: pip, uv, poetry. It resolves dependencies, manages virtual environments, and coordinates the build.
The build backend produces the actual package. It reads your source tree and pyproject.toml, then builds a wheel. Common backends: hatchling, setuptools, uv_build.
The backend is declared in pyproject.toml:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
When you run uv sync, uv resolves dependencies, then calls hatchling. Hatchling builds the wheel. uv installs it.
uv init originally defaulted to hatchling because it was a modern, standards-compliant backend with minimal dependencies. Since July 2025 it defaults to uv_build, uv’s own backend. Existing projects keep whatever they were initialized with.
The wheel pipeline
A .whl (wheel) is Python’s binary distribution format. It is a ZIP archive with the package files and a .dist-info metadata directory, structured so the installer extracts them directly into site-packages.
The flow:
pyproject.toml + src/
│
▼
build backend (build_wheel)
│
▼
.whl file
│
▼
installer extracts to site-packages/
The build backend reads the source. It always reads the source. Python packaging embeds the project build inside the dependency install.
Editable installs
When developing locally, you do not want to reinstall the project after every file change. PEP 660 defines the editable install: the build backend produces a wheel that, instead of copying files to site-packages, places a .pth file or proxy modules pointing back to your source directory.
uv sync installs the project as editable by default. The pipeline is the same: frontend calls build_editable() on the backend, but the output points to the source tree rather than duplicating it.
The build backend still reads the source. It has to, to determine which files belong in the wheel. The --no-install-project flag skips this step entirely, installing only third-party dependencies. That is what makes lockfile-first caching work for Python: install deps without the project, copy the source, then build the project in a separate layer.
References
- PEP 517: Build system independent format for source trees. Frontend/backend split
- PEP 427: The Wheel Binary Package Format.
.whlspecification - PEP 660: Editable installs.
build_editable()hook - uv: build backend. Choosing between
uv_buildand hatchling - uv: locking and syncing. Editable install behavior
- uv: sync command reference.
--no-install-project - hatchling. Build backend
- Why did uv use Hatch as a build backend?. Historical rationale
This post was written with AI assistance.