Build Backends
Build backends turn your source tree into distributable wheels and sdists. Hatchling is the modern default; setuptools remains common for legacy projects.
Recipe
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"uv buildWhen to reach for this:
- Configuring how your package is built
- Migrating from
setup.pytopyproject.toml - Building packages with extension modules
Working Example
[project]
name = "invoice-sdk"
version = "0.2.0"
requires-python = ">=3.14"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/invoice_sdk"]
[tool.hatch.build.targets.sdist]
include = ["src/", "tests/", "README.md"]uv build
tar -tzf dist/invoice-sdk-0.2.0-py3-none-any.whl | head
# invoice_sdk/__init__.py
# invoice_sdk/client.pyWhat this demonstrates:
hatchling.buildbackend specified in[build-system]packagestells hatchling where source livesuv buildproduces wheel and sdist- Wheel contents can be inspected before publish
Deep Dive
Backend Comparison
| Backend | Best for | Config |
|---|---|---|
| hatchling | New projects | [tool.hatch.*] |
| setuptools | Legacy, extensions | [tool.setuptools.*] |
| poetry-core | Poetry projects | [tool.poetry] |
| maturin | Rust extensions | [tool.maturin] |
| cffi/setuptools | C extensions | setup.py + pyproject |
Gotchas
- Missing
packagesconfig - empty wheel. Fix: configure package discovery in[tool.hatch.build]. - Wrong build backend for extensions - build fails. Fix: maturin for Rust, setuptools for C.
- Stale
setup.pyalongside pyproject - conflicting metadata. Fix: migrate fully; delete setup.py. - Building on wrong platform - platform-specific wheel on wrong OS. Fix: cibuildwheel for multi-platform.
- Not testing the built wheel - missing files discovered at publish. Fix:
pip install dist/*.whlbefore upload.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| setuptools | Existing setuptools config | Greenfield project |
| flit | Simple single-package | Complex monorepos |
| PDM backend | PDM-managed project | uv/hatchling shop |
FAQs
Which backend for new projects?
hatchling. Minimal config, fast, PEP 621 native.
Do I need setup.py?
No with hatchling or setuptools pyproject.toml config.
How do I build an sdist only?
uv build --sdist or python -m build --sdist.
Can I customize wheel contents?
hatchling force-include, exclude, and artifacts options.
How do I build editable?
pip install -e . or uv pip install -e . - not a wheel build.
What is PEP 517?
Standard for specifying build backends in pyproject.toml.
How do I add a build dependency?
[build-system] requires = ["hatchling", "cython"].
Monorepo builds?
Separate pyproject.toml per package or uv workspace with per-member builds.
How do I verify the wheel?
tar -tzf dist/*.whl or unzip -l dist/*.whl.
setuptools migration?
Move metadata to [project], configure [tool.setuptools.packages.find].
Related
- Packaging Basics - build intro
- Native Extensions & Wheels - compiled packages
- Publishing to PyPI - upload workflow
- pyproject.toml Explained
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.