Introduction

Migrating from setup.py to pyproject.toml for Python package builds follows PEP 517/518 standards, but the transition causes build failures when build-system requirements are not properly declared, dynamic metadata fields are not configured, or package discovery patterns do not match the original setup.py configuration. Modern pip uses isolated builds by default, which means build-time dependencies in setup.py are not available unless declared in build-system.requires. This causes ModuleNotFoundError during build and missing package metadata in the final distribution.

Symptoms

bash
ModuleNotFoundError: No module named 'setuptools_scm'
  File "/tmp/build-env-xyz/lib/python3.11/site-packages/setuptools/config/expand.py", line 123

Or build isolation errors:

bash
ERROR: Could not find a version that satisfies the requirement mypackage (from versions: none)
ERROR: No matching distribution found for mypackage

Or missing metadata:

bash
WARNING: Metadata file not found: pyproject.toml
Running setup.py install for mypackage ... error

Common Causes

  • build-system requires missing dependencies: setuptools_scm, wheel, or setuptools not declared
  • Dynamic fields not declared: version, dependencies read from files not marked as dynamic
  • Package discovery pattern changed: find_packages() behaves differently in pyproject.toml
  • Build backend not specified: No build-backend key in build-system table
  • setup.py still present and conflicting: Old setup.py interferes with pyproject.toml
  • Missing README or LICENSE: setuptools-scm requires these for version calculation

Step-by-Step Fix

Step 1: Configure build-system correctly

toml
# pyproject.toml
[build-system]
requires = ["setuptools>=68.0", "setuptools-scm>=8.0", "wheel"]
build-backend = "setuptools.build_meta"

Step 2: Configure project metadata and dynamic fields

```toml [project] name = "mypackage" description = "A useful Python package" requires-python = ">=3.9" license = {text = "MIT"} authors = [ {name = "Developer", email = "dev@example.com"} ] classifiers = [ "Development Status :: 4 - Beta", "Programming Language :: Python :: 3", ]

# Dynamic fields - read from files at build time dynamic = ["version", "dependencies", "optional-dependencies"]

[tool.setuptools] packages = {find = {where = ["src"], exclude = ["tests*"]}}

[tool.setuptools_scm] version_scheme = "post-release" local_scheme = "node-and-date" ```

Step 3: Handle dependencies from requirements files

```toml [tool.setuptools.dynamic] dependencies = {file = ["requirements.txt"]}

[tool.setuptools.dynamic.optional-dependencies] dev = {file = ["requirements-dev.txt"]} docs = {file = ["requirements-docs.txt"]}

# Or inline: [project.optional-dependencies] dev = ["pytest>=7.0", "black", "mypy"] docs = ["sphinx>=5.0", "sphinx-rtd-theme"] ```

Prevention

  • Keep setuptools>=68.0 in build-system requires for full pyproject.toml support
  • Use src/ layout to prevent accidental imports during installation
  • Declare all dynamic fields explicitly in the [project] table
  • Use python -m build instead of python setup.py sdist bdist_wheel
  • Remove setup.py entirely or keep it as a thin wrapper that imports setuptools
  • Test builds in a clean virtual environment to verify build isolation works
  • Pin build-system dependency versions to avoid breakage from upstream changes