44.2 Choosing a Build Backend: setuptools, Flit, Hatch, PDM
The choice of build backend is one of the most consequential decisions you will make when preparing a Python package for distribution. It dictates the format of your package’s metadata, how it is built into distribution artifacts (sdist and wheel), and often influences the entire developer experience. While setuptools is the longstanding incumbent, modern alternatives like Flit, Hatch, and PDM offer streamlined workflows and declarative configuration. The backend is specified in the build-system table in your pyproject.toml file, as defined in PEP 518, which ensures that pip and other tools can install the correct dependencies before attempting to build your package.
The Role of pyproject.toml and PEP 518
Historically, package metadata was scattered across setup.py, setup.cfg, and MANIFEST.in files. PEP 518 introduced the pyproject.toml file as a single, standard location for build system requirements and configuration. This file is read first by build tools, making it backend-agnostic. The build-system table is mandatory and tells the frontend (like pip) what backend to use and what dependencies it needs.
[build-system]
requires = ["setuptools>=64.0.0", "wheel"]
build-backend = "setuptools.build_meta"
This configuration is the default for a setuptools-based project. The requires key lists the packages that must be installed into the build environment to execute the build. The build-backend key points to the Python object that will perform the actual build (e.g., creating a sdist or wheel).
setuptools: The Established Workhorse
setuptools is the most traditional and widely used backend. Its immense flexibility allows it to handle incredibly complex package structures, including packages with C extensions. Configuration can be done in a dynamic setup.py script, a declarative setup.cfg, or within pyproject.toml (as of setuptools v62).
A minimal setup.py for a pure-Python package looks like this:
from setuptools import setup, find_packages
setup(
name="my_legacy_package",
version="0.1.0",
packages=find_packages(where="src"),
package_dir={"": "src"},
install_requires=[
"requests>=2.25.0",
],
python_requires=">=3.8",
)
Why you might choose it: You need maximum compatibility, are building packages with C extensions, or have a highly complex build process that requires the full programmatic power of a setup.py script.
Pitfall: The dynamic nature of setup.py can make it difficult to introspect package metadata without executing potentially arbitrary code, a security and performance concern that modern backends avoid.
Flit: Simplicity for Pure-Python Packages
Flit is designed explicitly for simplicity and pure-Python packages. It requires all package metadata to be statically defined in pyproject.toml, which allows tools to read the metadata quickly and safely without executing any code. It also simplifies the process by automatically including all files tracked in your version control system (like Git).
[build-system]
requires = ["flit_core>=3.4"]
build-backend = "flit_core.buildapi"
[project]
name = "my-flit-package"
authors = [
{name = "Jane Developer", email = "jane@example.com"}
]
description = "A sample package built with Flit."
requires-python = ">=3.7"
dependencies = [
"requests>=2.25.0",
]
dynamic = ["version"]
[tool.flit.module]
name = "my_flit_package"
# Using dynamic versioning from a __version__.py file
[tool.flit.version]
file = "my_flit_package/__version__.py"
Why you might choose it: Your package is pure-Python, you value a minimal and declarative configuration, and you want fast, reliable metadata introspection. Pitfall: It is not suitable for packages containing C extensions or those that require complex custom build steps.
Hatch: The Modern Meta-Project Manager
Hatch is more than just a build backend; it’s a comprehensive project management tool. Its build backend, hatchling, shares Flit’s philosophy of static metadata in pyproject.toml but is more feature-complete, natively supporting dynamic metadata via plugins and being capable of building packages with C extensions.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-hatch-package"
version = "0.1.0"
description = "A sample package built with Hatch."
authors = [
{name = "Jane Developer", email = "jane@example.com"}
]
dependencies = [
"requests>=2.25.0",
]
requires-python = ">=3.7"
[project.optional-dependencies]
test = [
"pytest>=6.0",
]
dev = [
"black",
"flake8",
]
Why you might choose it: You want a modern, powerful, and all-in-one tool that handles building, versioning, testing, and publishing seamlessly. Its environment management and robust versioning strategies are significant advantages. Pitfall: It has a steeper learning curve due to its extensive feature set beyond just building.
PDM: The Python Development Master
PDM is primarily a modern Python package manager (a la pip/poetry) that uses the pep517 build backend by default. However, it can also be used as a build backend itself (pdm-backend). Like Hatch and Flit, it favors static metadata configuration. A key innovation is its use of PEP 665 to produce deterministic, lockable installs for applications.
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[project]
name = "my-pdm-package"
version = "0.1.0"
description = "A sample package built with PDM."
authors = [
{name = "Jane Developer", email = "jane@example.com"}
]
dependencies = [
"requests>=2.25.0; python_version >= '3.8'",
"requests>=2.20.0; python_version < '3.8'"
]
requires-python = ">=3.7"
Why you might choose it: You are already using PDM to manage your project’s dependencies and environments and want a unified, consistent toolchain. It excels in managing complex dependency graphs for applications. Pitfall: It is less common as a standalone build backend compared to the others; most users adopt the full PDM toolchain.
Best Practices and Final Recommendation
- Prefer Static Metadata: Whenever possible, use a backend that allows you to define metadata statically in
pyproject.toml(Flit,Hatch,PDM, or modernsetuptools). This is faster, safer, and enables better tooling. - Match Backend to Project Complexity: For simple pure-Python packages,
Flitis excellent. For more complex needs or if you want a full-featured project manager, chooseHatch. If you need ultimate flexibility or have C extensions,setuptoolsremains the safe choice. - Test Your Build: Always test building your package into both a source distribution (sdist) and a wheel before attempting to upload to PyPI. This can be done with the
buildpackage:python -m pip install build python -m build - Be Explicit with
requires-python: Always set therequires-pythonfield to prevent users on incompatible Python versions from installing broken packages.
There is no single “best” backend; the optimal choice depends entirely on your project’s requirements and your preferred workflow. The critical takeaway is that PEP 518 allows you to choose any of these backends and confidently build your package for the Python ecosystem.