The Python packaging ecosystem has undergone a seismic shift in recent years. For over a decade, setup.py served as the de facto standard for defining project metadata and build requirements. However, the move toward declarative configuration via pyproject.toml has fundamentally changed how we think about software distribution. This transition, spearheaded by the Python Packaging Authority (PyPA), aims to eliminate ambiguity, enforce consistency, and enable better tool interoperability.
For intermediate to advanced developers, sticking with legacy workflows is no longer sustainable. Tools like Hatch and Build are leading the charge, offering streamlined experiences that replace complex shell scripts and custom build backends. In this guide, we will explore the motivations behind this migration, the structure of the new standard, and a practical roadmap for moving your project forward.
Why Move Away from setup.py?
The primary argument against setup.py is its imperative nature. It requires the file to be executed to read metadata, which introduces risks such as side effects during import and dependency resolution issues. In contrast, pyproject.toml is declarative. It uses the TOML format to clearly separate build requirements from runtime dependencies.
This separation is critical for modern build backends. When you use build to create a source distribution (sdist) or a wheel, the environment is isolated. If your setup.py relies on installing specific dependencies to run, it can break the build process in isolated environments. pyproject.toml ensures that the build system itself is defined explicitly, preventing "dependency hell" during the packaging phase.
Setting Up the Project with Hatch
Hatch is a highly modern Python packaging and project management tool that aligns perfectly with the new standards. It simplifies the entire lifecycle, from dependency management to version bumping and publishing.
To begin migration, ensure you have Hatch installed in your environment:
pip install hatch
Once installed, you can initialize a project. If you are starting fresh or converting an existing structure, Hatch can generate the foundational pyproject.toml file for you. This file is the heart of your new configuration, containing metadata, build requirements, and plugin settings.
Here is a robust example of what a migrated pyproject.toml looks like when configured with Hatch:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "modern-pkg"
version = "0.1.0"
description = "A modern Python package using pyproject.toml"
readme = "README.md"
requires-python = ">=3.9"
license = {text = "MIT"}
authors = [
{name = "Jane Doe", email = "jane@example.com"}
]
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0.0"
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"hatch"
]
[tool.hatch.build.targets.wheel]
packages = ["src/modern_pkg"]
[tool.hatch.build.targets.sdist]
include = ["src/modern_pkg", "tests"]
Note the clarity: all dependencies are listed under the [project] section, while build-specific configurations reside under [tool.hatch]. There is no imperative code to parse; it is pure data.
Building with the Modern Toolchain
With the configuration file in place, the old python setup.py sdist bdist_wheel commands are obsolete. Instead, we use the build module, which serves as the backend execution engine. However, Hatch simplifies this further.
To build your package for distribution, run the following command in your project root:
hatch build
This command triggers the build backend (Hatchling, in this case) to create artifacts in the dist/ directory. These artifacts are now ready to be uploaded to PyPI or tested locally.
If you prefer to stick strictly to the standard build module without the extra features of Hatch, you can still use it directly:
python -m build
This command will read your pyproject.toml, install the necessary build dependencies from the build-system section, and generate the distributions. The result is identical to Hatch, proving the interoperability of the ecosystem.
Practical Migration Strategy
Transitioning an existing project is best done incrementally. First, create the new pyproject.toml file by extracting your metadata from setup.py. Do not delete the old file immediately. Instead, test the build process using hatch build or python -m build to ensure the generated wheels match your old expectations.
Verify that your tests pass in the built artifacts. Check that all optional dependencies install correctly. Once validated, you can safely remove setup.py and setup.cfg (if used) to clean up your repository.
Conclusion
Migrating from setup.py to pyproject.toml is more than just a syntax change; it is an investment in the long-term health and maintainability of your Python projects. By leveraging tools like Hatch and the standard build backend, you adopt a workflow that is robust, standardized, and future-proof.
For developers ready to modernize their stack, the learning curve is minimal, but the benefits in terms of tooling consistency and ecosystem compatibility are substantial. Start by updating your next project's configuration today and embrace the future of Python packaging.