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`…

How to Build AI Agents with Python: A Practical Guide for Developers

How to Build AI Agents with Python: A Practical Guide for Developers

TL;DR: Building AI agents with Python is simpler than you think. This guide walks through setting up an… ...

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.

The Complete Guide to Outsourcing to Vietnam in 2026

The Complete Guide to Outsourcing to Vietnam in 2026

Vietnam has emerged as the premier destination for software outsourcing in 2026. With world-class engineering talent, competitive pricing,… ...

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.