The [build-system] table in pyproject.toml names the build backend and the packages needed to run it. Frontends such as pip and build read it, install those packages in an isolated environment, then call the backend to produce your sdist and wheel. Without it, your build setup is guesswork.
Definitions
A build frontend is the tool you run: python -m build, or pip when it installs from source. A build backend is the library that does the work of turning a source tree into distributions. The [build-system] table is the contract between the two.
The Python Packaging User Guide says this table should always be present, regardless of which backend you use. It has two keys that matter.
| Key | What it holds | Example |
|---|---|---|
| requires | Packages needed at build time, usually just the backend | ["hatchling"] |
| build-backend | The Python object the frontend calls | "hatchling.build" |
A minimal, working example
[build-system]
requires = ["setuptools >= 77.0.3"]
build-backend = "setuptools.build_meta"
The guide lists matching pairs for several common backends, including hatchling, setuptools, flit-core, PDM and uv-build. Copy the pair from your backend's own documentation rather than from memory, because the module path has to be exact.
What happens when someone installs from source
When a user installs your sdist, pip reads requires, creates a temporary isolated environment, installs those packages into it, and calls the backend named in build-backend. Frontends usually build in isolation, so a missing build dependency shows up as a build error rather than silently borrowing something from the user's machine.
That isolation is also a supply chain point. Everything in requires runs code on your machine and on every machine that builds your sdist. Treat that list like any other dependency list: keep it short, and constrain versions with intent.
Common mistakes
- Leaving the table out. Tools then fall back to legacy behaviour. State your backend explicitly.
- Forgetting a build-time plugin. If your backend needs a plugin to compute the version from Git, it belongs in
requires. - Loose pins on the backend. A lower bound such as
>= 77.0.3is normal. An unboundedrequiresmeans a future backend release can change how your package builds. - Confusing runtime and build dependencies.
requireshere is for building. Runtime dependencies go under[project].
What about setup.py?
The packaging guide states that setup.py and setuptools are not deprecated, but running python setup.py as a command is. Use python -m pip install . to install and python -m build to build, with setuptools as the backend if you like.
Check it yourself
Run python -m build in a clean virtual environment. If the build succeeds, both artifacts appear under dist, a source tarball and a wheel. If it fails with a missing module, the cause is almost always something absent from requires.
Safeguard's SBOM and dependency scanning can list what your library declares at runtime. Build requirements are worth reviewing separately, since they run during installs from source. You can try a free scan.
Quick check
Is the build-system table required?
The packaging guide says it is strongly recommended and should always be present. A source tree with no build configuration at all is treated as legacy and handled differently by tools.
Does requires list my runtime dependencies?
No. It lists what is needed to build the package. Runtime dependencies go in the dependencies field of the project table.
Why do I need to name a backend at all?
pip and build do not convert source to distributions themselves. A backend does that job, and the table tells the frontend which one to call.
Sources
- Writing your pyproject.toml, Python Packaging User Guide
- The Packaging Flow, Python Packaging User Guide
- Is setup.py deprecated?, Python Packaging User Guide
- Packaging Python Projects, Python Packaging User Guide
Last reviewed: 19 October 2026.