What is PEP 517/518 compatibility?
PEP 518 introduced pyproject.toml in 2016 and defined the [build-system] table for declaring build-time dependencies in a tool-agnostic way. PEP 517 followed in 2017 and defined the hook API that every build backend implements and every build frontend calls. Together they replaced the setup.py-only build model and made it possible to use Hatchling, flit_core, poetry-core, uv_build, maturin, or any other backend with the same install commands.
The problem the PEPs solved
Before PEP 518, every Python package was built by running setup.py, a Python script that imported setuptools (or the older distutils) and called functions to declare metadata, list dependencies, and produce build artifacts. The installer had to execute setup.py to discover anything about the package, including which build dependencies the script itself needed. That ordering created a circular requirement: setuptools had to already be present before the installer could ask the project what it required.
The setup.py model also coupled the entire ecosystem to setuptools. Alternative build systems existed but had to wrap or mimic the setuptools entry points to work with pip. Replacing setuptools meant replacing the whole installer integration, not just the build step.
What PEP 518 added
PEP 518 introduced the pyproject.toml file and reserved a [build-system] table for build configuration:
[build-system]
requires = ["setuptools>=61", "wheel"]The requires key lists packages the frontend must install into an isolated build environment before invoking the build. Because the file is static TOML, the frontend can read it without executing project code, and the build step starts with a known set of dependencies already in place.
pip 10.0 (April 2018) was the first installer to honor [build-system].requires. By that point, the file had a clear job even before any non-setuptools backends adopted it.
What PEP 517 added
PEP 517 extended the same [build-system] table with a build-backend key and defined the functions every backend must expose:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"build-backend names a Python object using the module[:object] syntax. The frontend imports that object and calls its hook functions, the most important of which are:
build_wheel(wheel_directory, ...)produces a wheel in the given directory.build_sdist(sdist_directory, ...)produces an sdist source distribution.get_requires_for_build_wheelandget_requires_for_build_sdistdeclare any extra dependencies the backend needs, beyond the staticrequireslist.prepare_metadata_for_build_wheellets a frontend extract metadata without producing the full wheel.
PEP 660 later added a build_editable hook so backends could implement editable installs the same way they implement regular wheels. pip 19.0 (January 2019) was the first release to support the full PEP 517 hook API; pip 21.3 (October 2021) added PEP 660 support.
How frontends and backends fit together
PEP 517 split package building into two halves: the build frontend (the tool a developer invokes) and the build backend (the library that produces artifacts).
A typical pip install . flow now looks like:
- pip reads
[build-system]frompyproject.toml. - pip creates an isolated build environment and installs the packages listed in
requires. - pip imports the object named in
build-backendand callsbuild_wheel. - The backend produces a wheel; pip installs it into the target environment.
Any compliant frontend can drive any compliant backend, so swapping Hatchling for flit_core, or pip for uv, changes which packages handle each step without changing the contract between them.
Map tools to each role
| Role | Tools |
|---|---|
| Build frontend | pip, uv, build, Hatch, Poetry, PDM |
| Build backend | setuptools, Hatchling, flit_core, poetry-core, uv_build, maturin, scikit-build-core |
Most projects do not need to think about the distinction once it is configured. pip install ., uv pip install ., and python -m build all go through the same interface; the project’s [build-system] table decides which backend gets called.
Spell out what compatibility means
A frontend is compatible if it reads the [build-system] table, installs the listed requires into an isolated environment, and calls the hooks defined by build-backend. A backend is compatible if it exposes the mandatory hook functions and can be imported from a freshly installed environment. Most active Python build tools have been compatible since 2019 or earlier; the question now is rarely whether a tool supports the standards but which optional hooks (PEP 660 editable installs, in-tree build backends, build isolation toggles) it implements.
Related
- What is a build backend? walks through what backends actually do.
- What is a build frontend? covers the other side of the interface.
- What is PEP 621 compatibility? explains how project metadata moved into the same file.
- What is PEP 660? standardized editable installs on top of PEP 517.
- Should I run
python setup.py? covers the deprecated commands these PEPs replaced. - pyproject.toml reference documents every table in the file, including
[build-system].