From Setup.py Chaos to Reproducible Builds: Why PyPI’s PEP 517/518 Matters
PyPI’s PEP 517/518 build system lets you declare a build backend and dependencies in pyproject.toml, creating isolated, reproducible wheels. Learn how to migrate from setup.py, see a concrete example, and weigh the trade‑offs.
02 Feb 2026, 09:38 UTC

Problem: Build‑time chaos in the old packaging world
When a Python project ships a setup.py, the build process runs inside the developer’s current environment. Any installed package, environment variable, or even a stray file can affect the resulting wheel. Even a small change in a local dependency can break the build on another machine, making reproducibility a nightmare.
Moreover, the build dependencies are not tracked in source control. They are pulled from the internet at build time, so the same setup.py can produce different artifacts over time.
Takeaway: Use pyproject.toml to lock the build environment
PEP 517/518 introduces a declarative build system that PyPI fully supports. By declaring the build backend and its build‑time dependencies in pyproject.toml, pip or python -m build creates a fresh virtual environment for each build. This guarantees that the wheel is built with the exact same tools every time, regardless of the developer’s local state.
Section 1 – The old way: dynamic, fragile builds
- Setup.py pulls dependencies from the internet each time it runs.
- Build dependencies are installed into the user’s environment, potentially overriding or being overridden by other packages.
- Side‑effects (e.g., a global environment variable) can leak into the wheel, leading to subtle bugs.
Section 2 – PEP 517/518: isolated, reproducible builds
PEP 517 defines a generic build interface. A project declares a build backend (e.g., setuptools.build_meta) and any build‑time dependencies in pyproject.toml. When pip installs a package from a source distribution, it performs the following steps:
- Creates a temporary virtual environment.
- Installs the declared build dependencies into that environment.
- Invokes the backend’s
build_wheelorbuild_sdistfunction. - Discards the environment after the wheel is produced.
This isolation prevents cross‑dependency contamination and ensures that the wheel contains only what the build backend produced.
Section 3 – Worked example
Below is a minimal project that demonstrates the full flow. All commands are run from the project root and require only a recent Python (≥3.10) and pip (≥22.3).
# Directory layout
mypkg/
├─ src/
│ └─ mypkg/
│ └─ __init__.py
├─ pyproject.toml
└─ README.md
pyproject.toml (pinning build dependencies):
[build-system]
requires = ["setuptools>=67", "wheel"]
build-backend = "setuptools.build_meta"
Build the package:
python -m build
# Produces dist/mypkg-0.1.0-py3-none-any.whl and dist/mypkg-0.1.0.tar.gz
Upload to TestPyPI:
twine upload --repository testpypi dist/*
Install from TestPyPI on a fresh machine:
pip install --index-url https://test.pypi.org/simple/ mypkg
In the pip log you’ll see a temporary virtual environment named something like _pip_build_env being created, the build dependencies installed, and the wheel built before the final installation. This confirms that the build happened in isolation.
Section 4 – Trade‑offs and limitations
- Build time and disk usage: Each upload recreates a virtual env, which can add seconds to the build pipeline and use extra disk space.
- Pinning required: If you omit version pins in
requires, pip may install newer, incompatible build tools, breaking reproducibility. - Older tooling: Some legacy packaging tools (e.g., legacy setuptools commands) may not support PEP 517, requiring a fallback or manual build.
- CI integration: Continuous integration services must have
python -m buildandtwineinstalled; older CI images might need manual steps.
Actionable next steps
- Replace any
setup.pywith apyproject.tomlthat declares a modern backend (setuptools.build_meta, poetry.core, flit_core, etc.). - Pin all build dependencies to exact versions.
- Run
python -m buildlocally to generate wheel and sdist. - Upload the artifacts with
twine uploadto PyPI or TestPyPI. - Verify installation with
pip installand inspect pip logs for the isolated build environment.
Adopting PEP 517/518 is a small change that dramatically improves build reproducibility, security, and future‑proofing for any Python project destined for PyPI.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.