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
| Feature | requirements.txt | pyproject.toml | setup.py |
|---|---|---|---|
| Primary purpose | Pin exact versions | Define package | Define package |
| Format | Plain text | TOML | Python |
| Version pinning | Primary use | Possible (not ideal) | Possible (not ideal) |
| Flexible ranges | Supported | Primary use | Primary use |
| Package metadata | No | Yes | Yes |
| Entry points/CLI | No | Yes | Yes |
| Tool config | No | Yes (Black, pytest, etc.) | No |
| Build distributions | No | Yes | Yes |
| PyPI publishing | No | Yes | Yes |
| Security | Safe (static) | Safe (static) | Risk (executable) |
| Learning curve | Trivial | Low | Medium |
| Future standard | Stable | PEP 621 standard | Deprecated |
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 themyclicommand- 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
- Add dependency: Edit
pyproject.toml - Lock versions: Run
pip-compile pyproject.toml -o requirements.txt - Commit both files
- Deploy: Use
pip install -r requirements.txt - 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:
- 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 = []
- 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
- 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.py | pyproject.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 file | Secondary file |
|---|---|---|
| Web app (Django, FastAPI, Flask) | requirements.txt | pyproject.toml (dev tools) |
| Reusable library | pyproject.toml | requirements.txt (CI only) |
| CLI tool | pyproject.toml | requirements.txt (deployment) |
| Data science notebook | requirements.txt | None needed |
| Internal utility package | pyproject.toml | requirements.txt (deployment) |
| Microservice | requirements.txt | pyproject.toml (dev tools) |
| Open source package | pyproject.toml | requirements.txt (CI only) |
Key Takeaways
- requirements.txt = reproducibility (exact versions, deployment)
- pyproject.toml = flexibility (version ranges, distribution)
- setup.py = legacy (still works, but use pyproject.toml for new projects)
- Using both = often the right answer for production projects
- 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.