Home/Blog/pyproject.toml vs requirements.txt vs setup.py - Which to Use
Software Engineering

pyproject.toml vs requirements.txt vs setup.py - Which to Use

Python packaging comparison: when to use pyproject.toml vs requirements.txt vs setup.py. Decision flowchart, migration guide, and how they work together for modern Python development.

By Inventive HQ Team
pyproject.toml vs requirements.txt vs setup.py - Which to Use

End the Python packaging confusion. Learn exactly when to use pyproject.toml, requirements.txt, or setup.py with our decision flowchart, real-world examples, and migration guides.

Python's packaging ecosystem has evolved significantly over the years, leaving many developers confused about which configuration file to use. Should you use pyproject.toml, requirements.txt, setup.py, or some combination? This guide cuts through the confusion with clear guidance for every project type.

Quick Answer: Decision Flowchart

Before diving into details, here's your quick reference:

                    What are you building?
                           │
         ┌─────────────────┼─────────────────┐
         ▼                 ▼                 ▼
   ┌───────────┐    ┌───────────┐    ┌───────────┐
   │  Library  │    │ Web App / │    │  Scripts  │
   │  (PyPI)   │    │    API    │    │ Notebooks │
   └───────────┘    └───────────┘    └───────────┘
         │                 │                 │
         ▼                 ▼                 ▼
  pyproject.toml    requirements.txt   requirements.txt
  (primary)         (primary)          (only)
         │                 │
         │                 ▼
         │          + pyproject.toml
         │          (for dev tools)
         ▼
  + requirements.txt
  (for CI pinning)

The short version:

  • Building a library for others? → pyproject.toml
  • Deploying an application? → requirements.txt
  • Need both? → Yes, that's often the right answer

The Three Files Explained

requirements.txt: Deployment Snapshots

requirements.txt is a simple text file that pins exact package versions:

flask==2.3.3
requests==2.31.0
gunicorn==21.2.0
sqlalchemy==2.0.23

Purpose: Create reproducible environments where every install is identical.

Best for:

  • Web applications (Django, FastAPI, Flask)
  • Deployed services and APIs
  • Docker containers
  • CI/CD pipelines that need reproducibility
  • Data science projects and notebooks

Key command: pip install -r requirements.txt

For a complete guide, see our Python requirements.txt tutorial.

pyproject.toml: Package Definition

pyproject.toml is Python's modern standard for project configuration:

[project]
name = "my-package"
version = "1.0.0"
dependencies = [
    "flask>=2.3",
    "requests>=2.28",
]

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

Purpose: Define package metadata and flexible dependency ranges.

Best for:

  • Reusable libraries published to PyPI
  • CLI tools
  • Internal packages shared across projects
  • Consolidating tool configuration (Black, pytest, mypy)

Key command: pip install . or pip install -e ".[dev]"

For a complete guide, see our pyproject.toml tutorial.

setup.py: Legacy Standard

setup.py is Python code that defines package metadata:

from setuptools import setup

setup(
    name="my-package",
    version="1.0.0",
    install_requires=["flask>=2.3", "requests>=2.28"],
)

Purpose: Same as pyproject.toml, but using executable Python.

Status: Deprecated for new projects (PEP 517/518), but still widely used.

When you'll see it:

  • Older packages not yet migrated
  • Projects with complex build requirements
  • Packages requiring C extensions with custom build logic

Feature Comparison Table

Featurerequirements.txtpyproject.tomlsetup.py
Primary purposePin exact versionsDefine packageDefine package
FormatPlain textTOMLPython
Version pinningPrimary usePossible (not ideal)Possible (not ideal)
Flexible rangesSupportedPrimary usePrimary use
Package metadataNoYesYes
Entry points/CLINoYesYes
Tool configNoYes (Black, pytest, etc.)No
Build distributionsNoYesYes
PyPI publishingNoYesYes
SecuritySafe (static)Safe (static)Risk (executable)
Learning curveTrivialLowMedium
Future standardStablePEP 621 standardDeprecated

Real-World Project Examples

Example 1: FastAPI Web Application

A typical FastAPI app needs reproducible deployments but also benefits from dev tool configuration.

Primary file: requirements.txt

fastapi==0.109.0
uvicorn[standard]==0.27.0
sqlalchemy==2.0.25
alembic==1.13.1
pydantic==2.5.3
python-jose[cryptography]==3.3.0

Secondary file: pyproject.toml (for dev tools)

[project]
name = "my-api"
version = "1.0.0"
requires-python = ">=3.10"

[project.optional-dependencies]
dev = ["pytest>=7.0", "httpx>=0.26", "black>=23.0", "ruff>=0.1"]

[tool.black]
line-length = 88

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

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"

Why this combination:

  • requirements.txt ensures production deploys are identical
  • pyproject.toml provides pip install -e ".[dev]" for developers
  • Tool configs live in one place

Example 2: Reusable Library (PyPI)

A library published to PyPI needs flexible dependencies so users can combine it with other packages.

Primary file: pyproject.toml

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

[project]
name = "my-library"
version = "2.0.0"
description = "A reusable Python library"
readme = "README.md"
license = "MIT"
requires-python = ">=3.9"
dependencies = [
    "requests>=2.28",
    "pydantic>=2.0",
]

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

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

Secondary file: requirements.txt (for CI only)

# Generated with: pip-compile pyproject.toml
requests==2.31.0
pydantic==2.5.3
# ... pinned transitive dependencies

Why this combination:

  • pyproject.toml with >= lets users resolve their own dependency tree
  • requirements.txt pins versions for CI reproducibility
  • Never ship requirements.txt to users

Example 3: Data Science Project

A data science project typically doesn't need distribution—just reproducibility.

Only file: requirements.txt

pandas==2.1.4
numpy==1.26.3
scikit-learn==1.3.2
matplotlib==3.8.2
jupyter==1.0.0
seaborn==0.13.1

Why requirements.txt only:

  • No package to distribute
  • Just need reproducible notebook environments
  • Simplest solution for the use case

Example 4: CLI Tool

A CLI tool needs entry points and should be distributable.

Primary file: pyproject.toml

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

[project]
name = "my-cli"
version = "1.0.0"
description = "A helpful CLI tool"
requires-python = ">=3.9"
dependencies = [
    "click>=8.1",
    "rich>=13.0",
]

[project.scripts]
mycli = "my_cli.main:app"

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

Why pyproject.toml primary:

  • [project.scripts] creates the mycli command
  • Users install with pip install my-cli
  • Development uses pip install -e ".[dev]"

Using Both Together: The Modern Workflow

The most common pattern for production projects is using both files together.

pyproject.toml as Source of Truth

# pyproject.toml - edit this
[project]
name = "my-app"
version = "1.0.0"
dependencies = [
    "fastapi>=0.109",
    "sqlalchemy>=2.0",
    "redis>=5.0",
]

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

Generate requirements.txt for Deployment

# Install pip-tools
pip install pip-tools

# Generate pinned requirements
pip-compile pyproject.toml -o requirements.txt

# For production with hashes (more secure)
pip-compile pyproject.toml -o requirements.txt --generate-hashes

This creates:

# requirements.txt - generated, don't edit manually
#
# This file is autogenerated by pip-compile with Python 3.11
#
annotated-types==0.6.0
anyio==4.2.0
fastapi==0.109.0
    # via my-app (pyproject.toml)
pydantic==2.5.3
    # via fastapi
redis==5.0.1
    # via my-app (pyproject.toml)
sqlalchemy==2.0.25
    # via my-app (pyproject.toml)
# ... more pinned dependencies

The Workflow

  1. Add dependency: Edit pyproject.toml
  2. Lock versions: Run pip-compile pyproject.toml -o requirements.txt
  3. Commit both files
  4. Deploy: Use pip install -r requirements.txt
  5. Develop: Use pip install -e ".[dev]"

Migrating Between Formats

From requirements.txt to pyproject.toml

When to migrate:

  • Building a distributable library
  • Want unified tool configuration
  • Creating a CLI with entry points

Steps:

  1. Create pyproject.toml:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-project"
version = "1.0.0"
requires-python = ">=3.9"
dependencies = []

[project.optional-dependencies]
dev = []
  1. Convert dependencies (loosen pins):
# requirements.txt         →  pyproject.toml
requests==2.31.0          →  "requests>=2.28"
flask==2.3.3              →  "flask>=2.3"
black==23.12.1            →  Move to [project.optional-dependencies] dev
pytest==7.4.4             →  Move to [project.optional-dependencies] dev
  1. Keep requirements.txt for deployment if needed

From setup.py to pyproject.toml

When to migrate:

  • Modernizing your package
  • Want tool config in one file
  • Preparing for Python ecosystem evolution

Mapping:

setup.pypyproject.toml
name="pkg"[project] name = "pkg"
version="1.0"[project] version = "1.0"
install_requires=[...][project] dependencies = [...]
extras_require={...}[project.optional-dependencies]
entry_points={"console_scripts": [...]}[project.scripts]
python_requires=">=3.9"[project] requires-python = ">=3.9"

Common Mistakes and Misconceptions

Mistake 1: "I must choose only one"

Reality: Most production projects benefit from both files.

  • pyproject.toml: source of truth, dev tools, editable installs
  • requirements.txt: deployment, CI/CD, Docker

Mistake 2: "pyproject.toml replaces requirements.txt"

Reality: They serve different purposes.

  • pyproject.toml says: "My package works with requests>=2.28"
  • requirements.txt says: "This exact environment uses requests==2.31.0"

A library author uses pyproject.toml. An operations engineer deploying that library uses requirements.txt.

Mistake 3: "setup.py is dead, I must migrate immediately"

Reality: setup.py still works fine. Migration is optional for existing projects.

  • New projects: use pyproject.toml
  • Existing projects: migrate when convenient, not urgent
  • Complex builds: setup.py may still be needed

Mistake 4: "Use requirements.txt for my library"

Reality: This causes dependency hell for your users.

If your library requires requests==2.31.0 (pinned), and another library requires requests==2.30.0, users can't install both. Use requests>=2.28 instead.

CI/CD Configuration Examples

GitHub Actions with requirements.txt

name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install -r requirements.txt
      - run: pytest

GitHub Actions with pyproject.toml

name: Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install -e ".[dev]"
      - run: pytest

Docker with requirements.txt

FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
CMD ["python", "-m", "my_app"]

Docker with pyproject.toml

FROM python:3.11-slim

WORKDIR /app
COPY pyproject.toml .
COPY src/ src/
RUN pip install --no-cache-dir .

CMD ["python", "-m", "my_app"]

Summary: Quick Reference

If you're building...Primary fileSecondary file
Web app (Django, FastAPI, Flask)requirements.txtpyproject.toml (dev tools)
Reusable librarypyproject.tomlrequirements.txt (CI only)
CLI toolpyproject.tomlrequirements.txt (deployment)
Data science notebookrequirements.txtNone needed
Internal utility packagepyproject.tomlrequirements.txt (deployment)
Microservicerequirements.txtpyproject.toml (dev tools)
Open source packagepyproject.tomlrequirements.txt (CI only)

Key Takeaways

  1. requirements.txt = reproducibility (exact versions, deployment)
  2. pyproject.toml = flexibility (version ranges, distribution)
  3. setup.py = legacy (still works, but use pyproject.toml for new projects)
  4. Using both = often the right answer for production projects
  5. pip-compile = bridge between them (generate requirements.txt from pyproject.toml)

For deep dives into each file format:

Elevate Your IT Efficiency with Expert Solutions

Transform Your Technology, Propel Your Business

Struggling with Python packaging decisions? InventiveHQ helps teams establish robust development workflows, CI/CD pipelines, and deployment strategies. Let us help you choose the right tools for your specific needs.

Discover Our Services

Frequently Asked Questions

Find answers to common questions

pyproject.toml defines flexible dependency ranges for distributable packages ('requests>=2.0'), while requirements.txt pins exact versions for reproducible deployments ('requests==2.28.1'). Use pyproject.toml when building libraries or tools for others to install; use requirements.txt when deploying applications where exact reproducibility is critical. Many projects use both together effectively.

Let's turn this knowledge into action

Get a free 30-minute consultation with our experts. We'll help you apply these insights to your specific situation.