Build Backends

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the Python Reference bibliography.

A build backend is the library named in [build-system] that a frontend (pip, build, Poetry, uv) calls through the PEP 517 hooks to produce an sdist and a wheel; frontends and backends are explained on Packaging Concepts & Standards. The backend decides what goes into the distributions and how the metadata is written; the [project] table is the same for all of them. This page builds birdtally (src layout, static [project] metadata, an MIT license, py.typed) with six backends, changing only [build-system], using build 1.6.1 on CPython 3.14.6 on 2026-10-22.

The pure-Python backends compared

Backend (version used) [build-system] PEP 639 since Metadata written Dynamic version sdist of birdtally (beyond PKG-INFO, pyproject.toml, README.md, LICENSE, src/)

setuptools 84.2.1

requires = ["setuptools>=77.0.3"], build-backend = "setuptools.build_meta"

77.0.3

2.4 (plus Dynamic: license-file)

[tool.setuptools.dynamic] version = {attr = "birdtally.__version__"}, or setuptools-scm

tests/, a generated setup.cfg and src/birdtally.egg-info/

hatchling 1.32.4

requires = ["hatchling>=1.27"], build-backend = "hatchling.build"

1.27.0

2.5

[tool.hatch.version] path = "src/birdtally/__init__.py", or hatch-vcs

everything Git does not ignore: tests/, examples/, CHANGELOG.md

flit-core 4.1.0

requires = ["flit_core>=3.12,<5"], build-backend = "flit_core.buildapi"

3.12

2.5

dynamic = ["version"] reads __version__ from the module, nothing else needed

nothing else (outside a Git checkout)

pdm-backend 2.5.0

requires = ["pdm-backend>=2.4"], build-backend = "pdm.backend"

2.4.0

2.4

[tool.pdm.version] source = "file" (or "scm")

tests/

poetry-core 2.5.0

requires = ["poetry-core>=2.2,<3"], build-backend = "poetry.core.masonry.api"

2.2.0

2.4

dynamic = ["version"] with the version in [tool.poetry] (or a plugin)

nothing else; include entries add files

uv_build 0.13.5

requires = ["uv_build>=0.13.5,<0.14"], build-backend = "uv_build"

0.7.19

2.4

none: with dynamic = ["version"] the build fails with missing field version

nothing else

All six built a wheel with the same four modules, py.typed and licenses/LICENSE, and all six worked with pip install -e (PEP 660 editable installs). Where they differ:

  • Metadata version: hatchling and flit-core already write Core Metadata 2.5 (which adds Import-Name); the others write 2.4. PyPI and twine 7.0.0 accept both.

  • sdist contents vary the most: compare tar tzf dist/*.tar.gz across backends before switching, and configure inclusion explicitly ([tool.hatch.build.targets.sdist], [tool.poetry] include, MANIFEST.in for setuptools).

  • Dynamic metadata: setuptools, hatchling, pdm-backend and flit-core can compute the version; poetry-core reads it from [tool.poetry]; uv_build accepts only static metadata.

  • Plugins: hatchling, pdm-backend, poetry-core and setuptools support plugins (for example VCS versions); flit-core and uv_build deliberately do not.

  • config-settings (python -m build -C key=value) are backend-specific. poetry-core accepts local-version (-C local-version=dev1 built birdtally-0.1.0+dev1-py3-none-any.whl), although build’s config-settings page says poetry-core supports none; setuptools wraps its command options in --build-option (-C--build-option=bdist_wheel -C--build-option=--build-number -C--build-option=123 built birdtally-0.1.0-123-py3-none-any.whl); hatchling, flit-core and uv_build take none. The next table has the details; see also Building sdists & Wheels.

Pin a lower bound for the features you use (PEP 639 needs the versions in the table) and, for backends whose authors recommend it (uv_build, poetry-core), an upper bound below their next breaking release.

File selection, config-settings and editable installs

What each backend puts into the distributions, which -C/--config-settings keys it reads, and how pip install -e works with it. The config-settings and editable columns were checked on 2026-10-22 with the same birdtally copies (pip 26.2.2 and build 1.6.1 on CPython 3.14.6, the same backend versions as above); the file-selection column is from each backend’s documentation.

Backend File selection config-settings Editable install (PEP 660)

setuptools 84.2.1

Packages by automatic discovery (src or flat layout) or [tool.setuptools.packages.find]; data files with package-data and include-package-data; extra sdist files through MANIFEST.in, or a plugin such as setuptools-scm (everything under version control).

Command options wrapped in --build-option (the build-number example above); editable_mode=strict or compat for editable installs.

Since v64.0.0. By default a __editable__.birdtally-0.1.0.pth that adds src/ to the path; -C editable_mode=strict installs a link tree under build/ instead, which does not pick up files added later.

hatchling 1.32.4

Per target ([tool.hatch.build.targets.wheel], [tool.hatch.build.targets.sdist]): include, exclude, only-include, packages, force-include, artifacts; files ignored by .gitignore/.hgignore are left out. Defaults: the sdist takes every file the VCS does not ignore, the wheel the package found from the project name (src/<name>/ among others).

None. hatchling’s hooks ignore the config_settings argument: -Creproducible=false still built a wheel with the fixed 2020-02-02 timestamps of a reproducible build. Set them in pyproject.toml instead: reproducible under [tool.hatch.build], shared-data under [tool.hatch.build.targets.wheel].

A _editable_impl_birdtally.pth that adds src/; dev-mode-dirs chooses the directories, dev-mode-exact = true uses an import hook instead (which IDEs and static analysis do not understand).

flit-core 4.1.0

Wheel: the module or package, data files included, .pyc files not; [tool.flit.module] name if it differs from the project name. sdist: the same plus pyproject.toml, the readme, the license files and the external data folder; [tool.flit.sdist] include/exclude for more. Adding the files committed in Git is a feature of the flit command, not of flit-core.

None, silently: -C foo=bar built the same wheel without a message.

Since 3.4: a birdtally.pth that adds src/.

pdm-backend 2.5.0

[tool.pdm.build] includes and excludes (wheel and sdist) and source-includes (sdist only, for tests and the like); by default the packages of the src or flat layout.

--python-tag, --py-limited-api, --plat-name, --build-number and no-clean-build: -C--build-number=7 built birdtally-0.1.0-7-py3-none-any.whl.

editable-backend = "path" (the default: a _editable_impl_birdtally.pth that adds src/) or "editables" (a proxy module, through the editables package).

poetry-core 2.5.0

[tool.poetry] packages, include (with format = "sdist"/"wheel") and exclude; files the VCS ignores are left out unless an include entry names them again.

local-version only (also poetry build --config-settings local-version=…).

A birdtally.pth that adds src/.

uv_build 0.13.5

Exactly one top-level module, module-name under module-root (default src); source-include, source-exclude, wheel-exclude and data under [tool.uv.build-backend]; __pycache__, *.pyc and *.pyo are always excluded.

None: any setting prints Warning: Config settings are not supported and is ignored.

A birdtally.pth that adds src/.

Every default editable install here is the same simple .pth path entry: new modules under src/ are importable at once, but changes to metadata (dependencies, entry points, the version) need a reinstall.

Stale statements in the documentation

  • The PUG tool recommendations and the build documentation’s backend explanation both say poetry-core uses [tool.poetry] instead of the standard [project] table. That was true before poetry-core 2.0 (2025); the poetry-core build above used only [project].

  • The PUG packaging tutorial pins uv_build >= 0.12.19, <0.13.0, while 0.13.5 is current and the uv documentation shows uv_build>=0.13.5,<0.14.

  • The build explanation’s setuptools snippet still lists "wheel" in requires, which modern setuptools does not need.

  • build’s config-settings page shows -Creproducible=true and -Cshared-data=/custom/path for hatchling, and says poetry-core supports no config settings. hatchling 1.32.4 ignores both settings (they are pyproject.toml options), and poetry-core reads local-version.

setuptools specifics

  • [tool.setuptools.dynamic] fills dynamic keys from attributes or files (version, readme, classifiers, …); using it for dependencies and optional-dependencies is marked BETA.

  • setup.py is not deprecated, running it is. A setup.py that only calls setup() (for C extensions or other programmatic configuration) is still supported, next to pyproject.toml; python setup.py sdist, bdist_wheel, install, develop and upload are deprecated or removed. Use python -m build and pip install. The upload, register and upload_docs commands are gone (setuptools 75.0.0, with the distutils merge).

  • pkg_resources was removed in setuptools 82.0.0 (2026-02-08); use importlib.metadata and importlib.resources.

  • To modernise a setup.py project: add pyproject.toml with [build-system], move static metadata into [project], keep setup.py only for what cannot be static, and list computed keys in dynamic.

Backends for extension modules

These backends were not run on this page (not run here): the last three columns are from each backend’s documentation, and the maturin settings also from its 1.15.0 source.

Backend (version) For File selection config-settings Editable install

setuptools

C and C++ natively, Rust and Go through plugins.

As for pure-Python projects (table above).

--build-option wrappers, for example build_ext --compiler.

As above; binary extensions may be a snapshot of the install time, so rebuild by reinstalling.

scikit-build-core 1.1.1

C, C++, Fortran and anything CMake builds.

sdist from .gitignore, adjusted with sdist.include/sdist.exclude; the Python packages in the wheel with wheel.packages.

Every [tool.scikit-build] option also works as a dotted setting: -Ccmake.define.CMAKE_BUILD_TYPE=Release, -Clogging.level=INFO (optionally prefixed skbuild.).

Supported, with an experimental rebuild-on-import mode and an inplace mode.

meson-python 0.22.1

C, C++, Fortran, Rust and the other languages Meson supports; common in scientific Python.

The sdist is meson dist: the latest commit in Git or Mercurial, without uncommitted changes or untracked files (export-ignore in .gitattributes drops more); the wheel holds what the meson.build install rules install.

setup-args, compile-args, install-args, dist-args, build-dir, editable-verbose.

Rebuilds on import; install with --no-build-isolation so that the build dependencies are still there at import time.

maturin 1.15.0

Rust (PyO3, cffi or uniffi bindings) through Cargo; worked example on Python Interop with PyO3.

[tool.maturin] include/exclude, python-source and python-packages for a mixed Rust/Python layout.

maturin.build-args (or the older build-args), for example -C maturin.build-args="--features fast", else the MATURIN_PEP517_ARGS environment variable.

Since 0.12.0 (pip install -e . or maturin develop); Python changes need no rebuild, Rust changes do (or an import hook).

Compiled extensions bring platform tags, manylinux and cibuildwheel into play: Binary Wheels & Extension Modules.

Poetry and other backends

Since Poetry 2.1, poetry build honours whatever [build-system] names: with another backend (or another poetry-core version than the one Poetry bundles) it builds in an isolated environment, like python -m build. Without a [build-system] table it still falls back to its bundled poetry-core with a warning, and the Poetry team has announced a switch to the PEP 517 default, setuptools, in a future release. A Poetry-managed project can therefore use hatchling or maturin for its builds and Poetry for dependencies.