Skip to main content

Guide to pyproject.toml: Modern Python Configuration

pyproject.toml is the standardized configuration file for Python projects, defined by PEP 518 and PEP 621. It serves as a single source of truth for project metadata, dependencies, build systems, and tool settings.

Why Use pyproject.toml?

Traditional vs. Modern

Historically, Python projects used multiple files (setup.py, setup.cfg, requirements.txt, .flake8). Modern projects consolidate these into pyproject.toml.

AspectTraditionalModern (pyproject.toml)
FormatPython code / INIDeclarative TOML
LocationMultiple filesSingle file
SecurityExecutable (Risk)Static (Safe)
Standardsetuptools specificTool-agnostic (PEP)

Core Sections

[build-system]

Defines how the project should be built.

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]

Standardized metadata (PEP 621).

[project]
name = "my-package"
version = "0.1.0"
description = "My awesome Python project"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [{name = "John Doe", email = "john@example.com"}]
dependencies = [
"requests>=2.28.0",
"click>=8.0.0",
]

[project.optional-dependencies]

Defines "extras" for development, testing, or optional features.

[project.optional-dependencies]
dev = ["pytest", "black", "ruff"]
docs = ["sphinx", "mkdocs"]

Tool Configuration

Most modern Python tools allow configuration directly within pyproject.toml.

Ruff (Linter & Formatter)

Ruff is a modern, fast (Rust-based) linter that replaces multiple traditional tools: Flake8, isort, Black, and more.

[tool.ruff]
line-length = 88
target-version = "py312"

[tool.ruff.lint]
select = [
"E", "F", "W", # Default flake8
"I", # isort (import sorting)
"UP", # pyupgrade
"S", # bandit (security)
"B", # bugbear
]

[tool.ruff.lint.isort]
known-first-party = ["my_package"]
force-sort-within-sections = true
combine-as-imports = true
lines-after-imports = 2
Key Import Rules
  • "I": Enables import sorting and grouping (replaces isort)
  • F401: Detects unused imports
  • F403: Detects from module import * (namespace pollution)

Pytest

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra -q"

Black

[tool.black]
line-length = 88
target-version = ['py311']

Best Practices

  1. Declarative First: Avoid using setup.py unless you have complex C-extensions that require custom build logic.
  2. Standard Metadata: Use the [project] section instead of tool-specific sections (like [tool.poetry]) when possible to remain portable.
  3. Pin Minimums: Use >= for library dependencies to allow flexibility, but use exact versions or lockfiles for applications.
  4. Tool Consolidation: Move all tool configs (Ruff, Mypy, Pytest) into pyproject.toml to reduce root directory clutter.

Migration Example

Old setup.py:

from setuptools import setup
setup(name="pkg", version="0.1", install_requires=["requests"])

New pyproject.toml:

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "pkg"
version = "0.1"
dependencies = ["requests"]