# How to migrate from setup.py to pyproject.toml


The `setup.py` file was long the dominant way to configure a Python package. [PEP 621](https://peps.python.org/pep-0621/) changed that by standardizing project metadata in [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md), and every major build backend now supports it. Migrating removes the need for executable configuration files, makes metadata readable by any tool, and aligns a project with current packaging standards.

This guide covers projects that use [setuptools](https://pydevtools.com/handbook/reference/setuptools.md) as their build backend. If a project uses Poetry, see [How to migrate from Poetry to uv](https://pydevtools.com/handbook/how-to/how-to-migrate-from-poetry-to-uv.md).

## Steps

### 1. Map setup.py fields to pyproject.toml

Most `setup()` arguments have a direct equivalent in `[project]` metadata. Here is a typical conversion:

Before (setup.py):

```python
from setuptools import setup, find_packages

setup(
    name="my-package",
    version="1.0.0",
    description="A short description",
    long_description=open("README.md").read(),
    long_description_content_type="text/markdown",
    author="Your Name",
    author_email="you@example.com",
    url="https://github.com/you/my-package",
    license="MIT",
    python_requires=">=3.9",
    install_requires=[
        "requests>=2.28",
        "click>=8.0",
    ],
    extras_require={
        "dev": ["pytest>=7.0", "ruff"],
    },
    packages=find_packages(where="src"),
    package_dir={"": "src"},
    entry_points={
        "console_scripts": [
            "my-cli=my_package.cli:main",
        ],
    },
)
```

After (pyproject.toml):

```toml
[build-system]
requires = ["setuptools>=77.0.3"]
build-backend = "setuptools.build_meta"

[project]
name = "my-package"
version = "1.0.0"
description = "A short description"
readme = "README.md"
license = "MIT"
requires-python = ">=3.9"
authors = [
    { name = "Your Name", email = "you@example.com" },
]
dependencies = [
    "requests>=2.28",
    "click>=8.0",
]

[project.optional-dependencies]
dev = ["pytest>=7.0", "ruff"]

[project.urls]
Homepage = "https://github.com/you/my-package"

[project.scripts]
my-cli = "my_package.cli:main"

[tool.setuptools.packages.find]
where = ["src"]
namespaces = false
```

Setting `namespaces = false` matches the behavior of `find_packages()`, which excludes implicit namespace packages. Without it, setuptools scans for namespace packages by default, which can change which directories are included.

The full field mapping:

| setup.py | pyproject.toml |
|---|---|
| `name` | `[project] name` |
| `version` | `[project] version` |
| `description` | `[project] description` |
| `long_description` + `long_description_content_type` | `[project] readme` |
| `author`, `author_email` | `[project] authors` |
| `url` | `[project.urls]` |
| `license` | `[project] license` |
| `python_requires` | `[project] requires-python` |
| `install_requires` | `[project] dependencies` |
| `extras_require` | `[project.optional-dependencies]` |
| `entry_points["console_scripts"]` | `[project.scripts]` |
| `packages` with `find_packages()` | `[tool.setuptools.packages.find]` |
| `package_dir` | `[tool.setuptools.package-dir]` |

### 2. Create the pyproject.toml file

Create `pyproject.toml` in the project root and add the converted configuration from step 1. Always include the `[build-system]` table so build tools know which backend to use:

```toml
[build-system]
requires = ["setuptools>=77.0.3"]
build-backend = "setuptools.build_meta"
```

> [!TIP]
> Setuptools 77.0.3+ is recommended for full `[project]` table support, including SPDX license strings like `license = "MIT"` (per [PEP 639](https://peps.python.org/pep-0639/)). Earlier versions (61.0+) support most of the `[project]` table but require the older `license = {text = "MIT"}` format.

### 3. Handle setup.cfg (if present)

Some projects split configuration between `setup.py` and `setup.cfg`. The `setup.cfg` fields map to the same `[project]` keys shown above. Move the packaging metadata into `pyproject.toml`.

If the project uses `setup.cfg` for tool configuration (like `[tool:pytest]`), move those to the corresponding `[tool.*]` sections in `pyproject.toml` where possible. Not all tools support `pyproject.toml` configuration; for example, flake8 only reads from `.flake8`, `setup.cfg`, or `tox.ini`. Check each tool's documentation before migrating its config. If any tool still requires `setup.cfg`, keep the file with only those sections.

```toml
[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
line-length = 88
```

### 4. Handle dynamic metadata

Some projects compute the version at build time from a VCS tag or a module attribute. Setuptools supports this through the `dynamic` field:

```toml
[project]
dynamic = ["version"]

[tool.setuptools.dynamic]
version = { attr = "my_package.__version__" }
```

For VCS-based versioning with `setuptools-scm`:

```toml
[build-system]
requires = ["setuptools>=77.0.3", "setuptools-scm>=8.0"]
build-backend = "setuptools.build_meta"

[project]
dynamic = ["version"]

[tool.setuptools_scm]
```

See [How to add dynamic versioning to uv projects](https://pydevtools.com/handbook/how-to/how-to-add-dynamic-versioning-to-uv-projects.md) for a complete walkthrough.

### 5. Verify the migration

Build the package to confirm the metadata is correct:

```console
$ uv build
```

Install the package in a virtual environment and verify the metadata looks correct:

```console
$ uv pip install dist/my_package-1.0.0-py3-none-any.whl
$ uv run python -c "
from importlib.metadata import metadata
m = metadata('my-package')
print(m['Name'], m['Version'])
print('Requires:', m.get_all('Requires-Dist'))
"
```

If the project has tests, run them to catch any packaging issues:

```console
$ uv run pytest
```

### 6. Remove legacy files

Once everything works, delete the files that are no longer needed:

If `setup.py` contains only a `setup()` call with static arguments (no programmatic logic), delete it. If it contains conditional logic, custom commands, or other programmatic configuration, keep it alongside `pyproject.toml`. Running `python setup.py` commands directly is deprecated, but `setup.py` is still a valid setuptools configuration file when programmatic configuration is needed.

Delete `setup.cfg` if all of its configuration has been moved to `pyproject.toml` or other config files.

> [!IMPORTANT]
> `MANIFEST.in` controls which non-package files (data files, documentation, configuration) are included in source distributions. `pyproject.toml` does not replace it. Before deleting `MANIFEST.in`, run `uv build` and inspect the sdist contents with `tar tf dist/*.tar.gz` to confirm all necessary files are included. If the project includes data files, documentation, or other non-Python assets in the sdist, keep `MANIFEST.in`.

## Switching to a different build backend

Migrating from `setup.py` to `pyproject.toml` does not require staying with setuptools. The `[project]` table is defined by [PEP 621](https://peps.python.org/pep-0621/) and supported by most major build backends. To switch to [hatchling](https://pydevtools.com/handbook/reference/hatch.md), for example, change the `[build-system]` table and replace any setuptools-specific configuration:

```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

The `[project]` metadata stays the same, but backend-specific tables like `[tool.setuptools.packages.find]` must be replaced with the new backend's equivalents. For a `src` layout with hatchling, replace the setuptools package-finding config with:

```toml
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
```

Each backend has its own conventions for source layout and build configuration. Consult the backend's documentation for details. See [What is a build backend?](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md) for guidance on choosing a backend.

## Learn More

- [setuptools pyproject.toml documentation](https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html)
- [PEP 621: Storing project metadata in pyproject.toml](https://peps.python.org/pep-0621/)
- [Should I run `python setup.py`?](https://pydevtools.com/handbook/explanation/should-i-run-python-setuppy-commands.md)
- [pyproject.toml reference](https://pydevtools.com/handbook/reference/pyproject.toml.md)
