From setup.py to pyproject.toml: A Practical Migration Guide with Poetry
Python projects still use fragile setup.py files. Switching to pyproject.toml centralizes metadata, separates packaging from runtime, and unlocks modern build backends like Poetry. This blog walks through the why, how, and trade‑offs with a concrete example and actionable next steps.
09 Sept 2025, 08:11 UTC

Problem: Fragile builds with setup.py
Most legacy Python projects still ship a setup.py that mixes build logic, metadata, and dependency resolution. This tight coupling leads to duplicate information, hard‑to‑trace build failures, and inconsistent installs across CI and user environments.
Thesis: pyproject.toml centralizes configuration and unlocks modern tooling
PEP 517 and PEP 518 introduced pyproject.toml, a declarative file that separates the packaging meta‑data from the runtime code. It allows build backends (e.g., Poetry, Flit, setuptools‑build) to read the configuration, build wheels, and resolve dependencies in a reproducible way. The result is cleaner codebases, fewer moving parts, and a smoother path to publishing on PyPI.
Section 1 – Declaring Metadata & Entry Points
In pyproject.toml you declare the project’s public information under the [project] table. For console scripts you use the [tool.poetry.scripts] table when using Poetry. Example:
[project]
name = "my‑tool"
version = "0.1.0"
description = "A CLI helper"
authors = ["Jane Doe <[contact removed]>"]
readme = "README.md"
requires-python = ">=3.9"
[project.urls]
homepage = "https://github.com/janedoe/my-tool"
[tool.poetry.scripts]
my-tool = "my_tool.cli:main"
Notice that the console script entry points are defined outside the [project] table because Poetry manages them in its own namespace.
Section 2 – Choosing a Build Backend
The [build-system] table tells pip which backend to use when installing the package. Poetry provides a drop‑in replacement for setuptools that handles dependency resolution and wheel creation.
[build-system]
requires = ["poetry-core>=1.3.0"]
build-backend = "poetry.core.masonry.api"
Other backends are possible:
- setuptools‑build – the traditional setuptools path, still supported but requires
setup.cfgfor metadata. - flit-core – minimalistic, great for simple libraries.
When you run pip install ., pip downloads the backend specified here, installs it in a temporary environment, and invokes it to produce a wheel.
Worked Example – A Minimal Poetry Project
Below is a minimal repository layout:
my-tool/
├─ pyproject.toml
├─ my_tool/
│ ├─ __init__.py
│ └─ cli.py
└─ tests/
└─ test_cli.py
Contents of my_tool/cli.py:
def main():
print("Hello from my‑tool!")
Build, test, and upload steps:
- Install Poetry in the build environment – required for the backend to run.
pip install poetry-core - Build a wheel – run in a clean virtual environment.
python -m pip install . - Verify the wheel – ensure metadata and entry points are correct.
pip show my-tool # Expected output includes Name, Version, Summary, Requires-Python, and Entry‑Points. - Run unit tests.
pytest - Upload to PyPI – after confirming the wheel is valid.
twine check dist/* # If all checks pass: twine upload dist/*
Key verification points:
- Running
pip install .in a fresh virtual environment should succeed without errors. - Inspect the generated wheel:
pip show my-toolshould list the console script underEntry‑Points. - Use
twine checkto validate the distribution before uploading.
Trade‑Offs & Limitations
- Backend installation – you must have the chosen backend (e.g., Poetry) available in the environment where pip will run. Missing it causes install failures.
- CI pipeline changes – existing build scripts that parse
setup.pyneed updates to readpyproject.tomlinstead. - Older pip support – pip 21.3 and earlier do not fully support PEP 517. If you need to support those versions, consider pinning a compatible backend or keeping a fallback
setup.py. - Non‑Python extensions – C extensions or custom build steps may require explicit configuration in the backend (e.g.,
tool.poetry.dependencieswithsystem‑requiredflags).
Actionable Closing – Next Steps
1. Add a pyproject.toml to your repo following the example above.
2. Remove or deprecate setup.py to avoid confusion.
3. In your CI, run python -m pip install . in a clean environment and run the test suite.
4. Once verified, publish with twine upload dist/*.
Adopting pyproject.toml may require a short learning curve, but the payoff is a more maintainable, reproducible build process that aligns with the modern Python ecosystem.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.