The definitive guide to Python's modern project configuration standard. Learn to configure pyproject.toml for packages, applications, and development tools with practical examples and troubleshooting tips.
If you've worked with Python projects, you've likely encountered the confusion of multiple configuration files: setup.py, setup.cfg, requirements.txt, MANIFEST.in, and various tool-specific configs like pytest.ini and .flake8. The Python community recognized this fragmentation and created a solution: pyproject.toml.
This comprehensive guide covers everything you need to know about pyproject.toml, from basic setup to advanced configurations, with real-world examples you can copy and adapt for your projects.
What Is pyproject.toml?
pyproject.toml is Python's standardized configuration file for project metadata, build settings, and tool configurations. Introduced through a series of Python Enhancement Proposals (PEPs), it serves as a single source of truth for your Python project:
- PEP 517 (2015) - Defined a build-system independent format for source trees
- PEP 518 (2016) - Introduced pyproject.toml and the
[build-system]table - PEP 621 (2020) - Standardized the
[project]table for metadata
Before pyproject.toml, you needed setup.py (executable Python) for packaging, which posed security risks and made dependency resolution difficult. pyproject.toml is declarative (static TOML), making it safer, faster to parse, and easier to understand.
Quick Start: Minimal pyproject.toml
Here's the simplest pyproject.toml that actually works:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-package"
version = "1.0.0"
description = "A simple Python package"
requires-python = ">=3.9"
dependencies = [
"requests>=2.28",
]
With this file in your project root, you can:
# Install your package in development mode
pip install -e .
# Build a distributable wheel
pip install build
python -m build
The Three Core Tables
pyproject.toml has three main sections, each serving a distinct purpose.
[build-system] - How Your Package Gets Built
The [build-system] table tells pip which tool to use when building your package:
[build-system]
requires = ["hatchling>=1.21"]
build-backend = "hatchling.build"
- requires: Packages needed to build (installed in isolated environment)
- build-backend: The module that performs the build
Build Backend Comparison
| Backend | Best For | Key Features | Config Complexity |
|---|---|---|---|
| Hatchling | New projects | Fast builds, minimal config, good defaults | Low |
| setuptools | Legacy migration | Most compatible, feature-rich | Medium |
| Flit | Pure Python | Simplest possible, no extras | Very Low |
| Poetry-core | Poetry users | Lock file integration | Medium |
Hatchling example:
[build-system]
requires = ["hatchling>=1.21"]
build-backend = "hatchling.build"
setuptools example:
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
Flit example:
[build-system]
requires = ["flit_core>=3.9"]
build-backend = "flit_core.buildapi"
Poetry-core example:
[build-system]
requires = ["poetry-core>=1.9"]
build-backend = "poetry.core.masonry.api"
[project] - Your Package Metadata (PEP 621)
The [project] table contains all metadata about your package:
[project]
name = "my-awesome-package"
version = "2.1.0"
description = "A package that does awesome things"
readme = "README.md"
license = "MIT"
requires-python = ">=3.9"
authors = [
{ name = "Your Name", email = "[email protected]" }
]
maintainers = [
{ name = "Maintainer", email = "[email protected]" }
]
keywords = ["python", "awesome", "package"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"requests>=2.28",
"click>=8.0",
"pydantic>=2.0",
]
[project.urls]
Homepage = "https://github.com/you/my-awesome-package"
Documentation = "https://my-awesome-package.readthedocs.io"
Repository = "https://github.com/you/my-awesome-package"
Changelog = "https://github.com/you/my-awesome-package/blob/main/CHANGELOG.md"
Optional Dependencies
Group dependencies for different use cases:
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"black>=23.0",
"ruff>=0.1",
"mypy>=1.0",
"pre-commit>=3.0",
]
test = [
"pytest>=7.0",
"pytest-cov>=4.0",
"pytest-asyncio>=0.21",
]
docs = [
"sphinx>=7.0",
"sphinx-rtd-theme>=2.0",
"myst-parser>=2.0",
]
Install specific groups:
pip install -e ".[dev]" # Development dependencies
pip install -e ".[test]" # Testing only
pip install -e ".[dev,docs]" # Multiple groups
Entry Points (Console Scripts)
Create command-line tools:
[project.scripts]
my-cli = "my_package.cli:main"
my-tool = "my_package.tools:run"
After installation, users can run my-cli and my-tool directly from the terminal.
For GUI applications (no terminal window on Windows):
[project.gui-scripts]
my-gui-app = "my_package.gui:main"
[tool.*] - Tool Configuration
Configure development tools in one place:
Black (Code Formatter)
[tool.black]
line-length = 88
target-version = ["py39", "py310", "py311", "py312"]
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
Ruff (Fast Linter)
[tool.ruff]
line-length = 88
target-version = "py39"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = ["E501"] # line too long (handled by formatter)
[tool.ruff.lint.isort]
known-first-party = ["my_package"]
pytest (Testing)
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_functions = ["test_*"]
addopts = [
"-v",
"--tb=short",
"--strict-markers",
]
markers = [
"slow: marks tests as slow",
"integration: marks integration tests",
]
filterwarnings = [
"ignore::DeprecationWarning",
]
mypy (Type Checking)
[tool.mypy]
python_version = "3.9"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_defs = false
Coverage
[tool.coverage.run]
source = ["my_package"]
branch = true
omit = ["*/tests/*", "*/__pycache__/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if TYPE_CHECKING:",
]
fail_under = 80
Dynamic Metadata
Some fields can be computed at build time instead of hardcoded.
Version from Source Code
Keep version in one place (my_package/__init__.py):
__version__ = "2.1.0"
Reference it in pyproject.toml:
[project]
name = "my-package"
dynamic = ["version"]
[tool.hatchling.version]
path = "my_package/__init__.py"
Version from Git Tags
Use git tags as the source of truth:
[project]
name = "my-package"
dynamic = ["version"]
[tool.hatchling.version]
source = "vcs"
[tool.hatch.build.hooks.vcs]
version-file = "my_package/_version.py"
Complete Examples by Project Type
Library Package (for PyPI)
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-library"
version = "1.0.0"
description = "A reusable Python library"
readme = "README.md"
license = "MIT"
requires-python = ">=3.9"
authors = [{ name = "Your Name", email = "[email protected]" }]
classifiers = [
"Development Status :: 4 - Beta",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
]
dependencies = [
"requests>=2.28",
]
[project.optional-dependencies]
dev = ["pytest>=7.0", "black>=23.0", "mypy>=1.0"]
[project.urls]
Homepage = "https://github.com/you/my-library"
[tool.black]
line-length = 88
[tool.pytest.ini_options]
testpaths = ["tests"]
FastAPI Application
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-api"
version = "1.0.0"
description = "FastAPI application"
requires-python = ">=3.10"
dependencies = [
"fastapi>=0.109",
"uvicorn[standard]>=0.27",
"pydantic>=2.5",
"sqlalchemy>=2.0",
"alembic>=1.13",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-asyncio>=0.23",
"httpx>=0.26", # For testing FastAPI
"black>=23.0",
"ruff>=0.1",
]
postgres = ["psycopg2-binary>=2.9"]
mysql = ["pymysql>=1.1"]
[project.scripts]
start-api = "my_api.main:start"
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
CLI Tool
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-cli-tool"
version = "2.0.0"
description = "A powerful command-line tool"
readme = "README.md"
license = "MIT"
requires-python = ">=3.9"
dependencies = [
"click>=8.1",
"rich>=13.0",
"httpx>=0.26",
]
[project.scripts]
mycli = "my_cli_tool.main:cli"
[project.optional-dependencies]
dev = ["pytest>=7.0", "pytest-click>=1.1"]
Migrating from setup.py
Before (setup.py)
from setuptools import setup, find_packages
setup(
name="my-package",
version="1.0.0",
description="My package description",
author="Your Name",
author_email="[email protected]",
packages=find_packages(),
install_requires=[
"requests>=2.28",
"click>=8.0",
],
extras_require={
"dev": ["pytest>=7.0", "black>=23.0"],
},
entry_points={
"console_scripts": [
"mycli=my_package.cli:main",
],
},
python_requires=">=3.9",
)
After (pyproject.toml)
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "1.0.0"
description = "My package description"
requires-python = ">=3.9"
authors = [{ name = "Your Name", email = "[email protected]" }]
dependencies = [
"requests>=2.28",
"click>=8.0",
]
[project.optional-dependencies]
dev = ["pytest>=7.0", "black>=23.0"]
[project.scripts]
mycli = "my_package.cli:main"
[tool.setuptools.packages.find]
where = ["."]
Migration Mapping
| setup.py | pyproject.toml |
|---|---|
name | [project] name |
version | [project] version |
description | [project] description |
author / author_email | [project] authors |
install_requires | [project] dependencies |
extras_require | [project.optional-dependencies] |
entry_points["console_scripts"] | [project.scripts] |
python_requires | [project] requires-python |
packages | [tool.setuptools.packages.find] |
Working with Both Files
Many projects use pyproject.toml as the source of truth but generate requirements.txt for deployment. For a complete guide on requirements.txt, see our Python requirements.txt guide.
Generating requirements.txt
# Using pip-tools (recommended)
pip install pip-tools
pip-compile pyproject.toml -o requirements.txt
# With hashes for security
pip-compile pyproject.toml -o requirements.txt --generate-hashes
# Using Poetry
poetry export -f requirements.txt --output requirements.txt --without-hashes
# Using PDM
pdm export -o requirements.txt --without-hashes
Workflow
- Edit dependencies in pyproject.toml
- Run
pip-compile pyproject.toml -o requirements.txt - Commit both files
- Deploy using requirements.txt
For a detailed comparison of when to use each file, see our pyproject.toml vs requirements.txt comparison.
Troubleshooting Common Errors
"Preparing metadata (pyproject.toml) did not run successfully"
Causes:
- Invalid TOML syntax
- Missing
[build-system]table - Incorrect
build-backendpath - Missing required fields in
[project]
Solutions:
# Validate TOML syntax
python -c "import tomllib; tomllib.load(open('pyproject.toml', 'rb'))"
# Check for common issues:
# 1. Ensure [build-system] exists
# 2. Verify build-backend matches requires (hatchling -> hatchling.build)
# 3. Ensure [project] has 'name' and 'version' (or 'dynamic')
"Failed building wheel for package"
Causes:
- Missing native build tools (gcc, Visual Studio)
- Platform-specific package without wheel
- Incompatible Python version
Solutions:
# On Ubuntu/Debian
sudo apt install build-essential python3-dev
# On macOS
xcode-select --install
# On Windows - install Visual Studio Build Tools
# Try installing with verbose output
pip install -v .
License Configuration Errors
PEP 639 changed license handling. Use the new format:
# New format (PEP 639)
[project]
license = "MIT"
# Or with file reference
[project]
license = {file = "LICENSE"}
"Invalid version" Errors
Versions must follow PEP 440:
# Valid versions
version = "1.0.0"
version = "2.0.0a1" # Alpha
version = "2.0.0b2" # Beta
version = "2.0.0rc1" # Release candidate
version = "2.0.0.post1" # Post release
version = "2.0.0.dev1" # Development
# Invalid versions
version = "1.0" # OK but prefer 1.0.0
version = "v1.0.0" # No 'v' prefix
version = "1.0.0-beta" # Use 1.0.0b1 instead
Best Practices
Always Specify requires-python
Prevents cryptic errors on incompatible Python versions:
[project]
requires-python = ">=3.9"
Use Optional Dependencies for Dev Tools
Keep production installs lean:
[project.optional-dependencies]
dev = ["pytest", "black", "mypy", "ruff"]
Pin Build Backend Versions
Prevent unexpected build failures:
[build-system]
requires = ["hatchling>=1.21,<2"] # Pin major version
Use src/ Layout for Packages
Prevents accidental imports of uninstalled code:
my-project/
├── pyproject.toml
├── src/
│ └── my_package/
│ ├── __init__.py
│ └── module.py
└── tests/
└── test_module.py
[tool.setuptools.packages.find]
where = ["src"]
Include py.typed for Type Hints
Signal that your package supports type checking:
my_package/
├── __init__.py
├── py.typed # Empty file
└── module.py
Security Considerations
Review Transitive Dependencies
# See all dependencies (including transitive)
pip install pipdeptree
pipdeptree
# Audit for known vulnerabilities
pip install pip-audit
pip-audit
Use Hash Checking
pip-compile pyproject.toml -o requirements.txt --generate-hashes
Summary
pyproject.toml is Python's modern standard for project configuration, replacing the fragmented landscape of setup.py, setup.cfg, and various tool-specific config files. Key takeaways:
- [build-system] defines how to build your package (use Hatchling for new projects)
- [project] contains metadata and dependencies (PEP 621 standard)
- [tool.*] consolidates configuration for Black, Ruff, pytest, mypy, and more
- Use optional-dependencies to separate dev, test, and docs requirements
- Generate requirements.txt from pyproject.toml for deployment reproducibility
For deciding when to use pyproject.toml versus requirements.txt, see our complete comparison guide. For mastering requirements.txt specifically, check out our requirements.txt guide.
Elevate Your IT Efficiency with Expert Solutions
Transform Your Technology, Propel Your Business
Building Python applications at scale? InventiveHQ provides expert consulting for Python development best practices, CI/CD pipelines, and cloud deployments. Our team can help you establish robust packaging workflows and development standards.