Why I Ditched setup.py for pyproject.toml: A Python Developer’s Migration Guide

1 comment
(Developer Tutorials) - If you're still using setup.py for your Python projects, you're fighting an outdated standard. Here's exactly how to migrate to pyproject.toml and why your future self will thank you.

Why I Ditched setup.py for pyproject.toml: A Python Developer’s Migration Guide

I’ll be honest. I fought the pyproject.toml migration for almost two years.

It felt unnecessary. *”If it ain’t broke, don’t fix it,”* right? My setup.py files worked fine. I knew exactly where every configuration lived. Then I inherited a project with seven different configuration files, each doing something slightly different. `setup.py`, `requirements.txt`, `setup.cfg`, `tox.ini`, `pytest.ini`, `.flake8`…

Outsourcing Software in 2025: Why Vietnam Beats India and Philippines for Elite Engineering Teams

Outsourcing Software in 2025: Why Vietnam Beats India and Philippines for Elite Engineering Teams

TL;DR: Vietnam is quietly becoming the #1 destination for serious outsourcing software engineering. Lower attrition, stronger English, and… ...

That’s when the pain hit. Real pain.

I migrated everything to a single `pyproject.toml` file in one afternoon. And here’s exactly how you can do it too.

Vietnam Outsourcing in 2025: Why Smart CTOs Are Betting on Southeast Asia’s Rising Tech Hub

Vietnam Outsourcing in 2025: Why Smart CTOs Are Betting on Southeast Asia’s Rising Tech Hub

TL;DR – What’s This About? Vietnam outsourcing is no longer a "budget backup" — it’s a strategic advantage.… ...

Why pyproject.toml Won (And Why setup.py Lost)

The Python ecosystem has been moving toward PEP 621 for years. The goal is simple: standardize project metadata in one place. No more guessing which file holds your package name or dependencies.

Here’s what you gain:

  • Unified config — One file for your build system, linter, formatter, and test runner
  • Deterministic builds — No more `setup.py` executing arbitrary code during installation
  • Tool interoperability — Black, Ruff, mypy, pytest all read from the same file
  • Cleaner dependency management — PEP 508 compliant, no more parsing `requirements.txt` manually

Don’t believe me? Let’s compare.

The Old Way (setup.py + friends)

python
# setup.py
from setuptools import setup, find_packages

setup(
    name="my-cool-project",
    version="0.1.0",
    packages=find_packages(),
    install_requires=[
        "fastapi>=0.100.0",
        "uvicorn[standard]>=0.23.0",
        "sqlalchemy>=2.0.0",
        "pydantic>=2.0.0",
    ],
    extras_require={
        "dev": ["pytest>=7.0.0", "black>=23.0.0", "ruff>=0.1.0"],
        "test": ["pytest>=7.0.0", "coverage>=7.0.0"],
    },
    python_requires=">=3.11",
)

That’s fine. But then you also need:

  • `requirements-dev.txt`
  • `setup.cfg` (if you’re fancy)
  • `pyproject.toml` anyway (for build tools)

It’s fragmented. Honestly, it’s a mess.

The New Way (Single pyproject.toml)

toml
[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.backends._legacy:_Backend"

[project]
name = "my-cool-project"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.100.0",
    "uvicorn[standard]>=0.23.0",
    "sqlalchemy>=2.0.0",
    "pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "black>=23.0.0",
    "ruff>=0.1.0",
]
test = [
    "pytest>=7.0.0",
    "coverage>=7.0.0",
]

[tool.black]
line-length = 100
target-version = ["py311"]

[tool.ruff]
line-length = 100
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]

[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]

Clean. Complete. One file.

The Step-by-Step Migration Guide

If you’re ready to migrate an existing project, here’s the exact process I use.

Step 1: Audit Your Current Configuration

First, list every config file you currently use. I bet it’s more than you think.

bash
find . -maxdepth 1 -type f | grep -E '\.(cfg|ini|toml|txt|flake8)$'

You’ll probably see: `setup.py`, `setup.cfg`, `requirements.txt`, `tox.ini`, `pytest.ini`, `.flake8`, `mypy.ini`.

Step 2: Copy All Metadata Into pyproject.toml

Take everything from `setup.py`’s `setup()` call and move it under the `[project]` table. The keys map almost directly:

setup.py parameter pyproject.toml key
`name` `[project].name`
`version` `[project].version`
`description` `[project].description`
`install_requires` `[project].dependencies`
`extras_require` `[project].optional-dependencies`
`python_requires` `[project].requires-python`
`packages` `[tool.setuptools.packages.find]`

Step 3: Move Tool Configs Under Their Respective Tables

Each tool has a dedicated section. The pattern is always `[tool.]`.

toml
[tool.black]
line-length = 100

[tool.ruff]
# Ruff configuration here

[tool.mypy]
python_version = "3.11"
strict = true

Pro tip: Check each tool’s documentation for the exact keys. Most modern tools (Ruff, Black, mypy, pytest) fully support pyproject.toml now.

Step 4: Replace setup.py with a No-Op

You can keep a minimal `setup.py` for backwards compatibility:

python
from setuptools import setup

setup()

Or delete it entirely if you’re not using `pip install -e .` (which still works with pyproject.toml).

Step 5: Test Your Build

bash
pip install -e .
python -c "import my_cool_project; print('It works!')"

Then run your tests:

bash
pytest
ruff check .
black --check .

If everything passes, you’re done. Delete the old config files.

Handling Dynamic Dependencies (The Tricky Part)

Not everything is straightforward. Sometimes you need dynamic dependencies based on the environment.

Old setup.py:

python
import sys

if sys.platform == "win32":
    install_requires.append("pywin32>=300")

In pyproject.toml, you handle this with environment markers:

toml
[project]
dependencies = [
    "pywin32>=300; platform_system == 'Windows'",
]

PEP 508 environment markers are powerful. Use `os_name`, `sys_platform`, `platform_machine`, `python_version`, and more.

What About Poetry and PDM?

You don’t have to use setuptools. Poetry and PDM have their own `pyproject.toml` conventions, but the standard `[project]` table is the same.

Poetry example:

toml
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "my-cool-project"
version = "0.1.0"

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.100.0"

The key difference: Poetry uses `[tool.poetry]` for metadata instead of `[project]`. But PEP 621 compliant tools use `[project]` directly. Pick one ecosystem and stick with it.

Real Talk: What I Learned from This Migration

Recently, we onboarded a new developer on a project I’d already migrated. She opened the repo and said, *”Oh, this is clean. Where’s the config?”*

That’s the point.

When you hand a pyproject.toml project to a remote team — like the talented engineers we work with in Ho Chi Minh City — there’s zero friction. No one asks *”Which file do I edit for the linter?”* It’s obvious.

I’ve seen this firsthand with our extended teams at ECOA AI. When we ship a Python project to one of our Vietnamese development teams, the first thing they check is the `pyproject.toml`. It tells them everything they need to know about the project’s standards. No guesswork.

When Should You NOT Use pyproject.toml?

To be fair, there are edge cases:

  • Legacy projects stuck on Python 3.8 or older — Some tools don’t support pyproject.toml fully on older Python versions
  • C-extensions with complex builds — If you’re doing weird things with `setuptools.Extension`, you might need `setup.py` for now
  • Extremely complex custom build steps — The `[tool.setuptools]` hooks are improving, but some edge cases still need a setup.py fallback

But honestly? 95% of projects can migrate today. The other 5% are edge cases you’ll know when you see.

Your Migration Checklist

Here’s a quick reference for your next migration:

  • Move all `setup()` arguments to `[project]` table
  • Convert `extras_require` to `[project.optional-dependencies]`
  • Replace `requirements.txt` with `[project.dependencies]`
  • Move linter configs to `[tool.]` sections
  • Add `[build-system]` table
  • Test with `pip install -e .`
  • Run full test suite
  • Delete old config files
  • Update README with new instructions
  • Celebrate that you never have to edit a setup.py again

Frequently Asked Questions

How do I migrate from setup.py to pyproject.toml for an existing project?

Start by creating a `pyproject.toml` file with a `[build-system]` table (using `setuptools` as the backend). Copy all metadata from your `setup()` call into the `[project]` table. Move tool configurations under `[tool.toolname]` sections. Test with `pip install -e .`, then delete the old config files.

Can I still use setuptools with pyproject.toml?

Yes. Setuptools fully supports `pyproject.toml` as a configuration source. You can remove `setup.py` entirely and let setuptools read from `pyproject.toml`. Just set `build-backend = “setuptools.backends._legacy:_Backend”` in your `[build-system]` table.

Do I need to change my project structure for pyproject.toml?

No. Your project structure stays the same. `pyproject.toml` only replaces the configuration files. Your `src/` layout, tests, and module organization remain unchanged. The migration is purely about config consolidation.

How do I add entry points (CLI scripts) with pyproject.toml?

Use the `[project.scripts]` table. For example: `[project.scripts]\nmy-cli = “my_package.cli:main”`. This replaces the `entry_points` parameter in `setup.py`. It works identically for console scripts and GUI scripts.

Related reading: Why Outsourcing Software Development in Vietnam Is the Smartest Move for Your Startup (and Your Sanity)

Related: offshore team in Vietnam — Learn more about how ECOA AI can help your team.

Related: Vietnam software outsourcing — Learn more about how ECOA AI can help your team.

Related: Vietnam offshore development — Learn more about how ECOA AI can help your team.

Related: Vietnam outsourcing — Learn more about how ECOA AI can help your team.

Related reading: Why You Should Hire Vietnamese Developers: A No-Nonsense Strategy Guide

Leave a Comment

Your email address will not be published. Required fields are marked *

Ready to Build with AI-Powered Developers?

Hire Vietnamese engineers augmented by ECOA AI Platform + Claude Code. 5x faster, 40% cheaper.