cheat sheet
pyproject.toml
The canonical config file for modern Python projects — [project] metadata, [build-system] backends (setuptools, hatchling, pdm-backend, poetry-core, flit-core), [tool.*] sections, dynamic versioning, scripts, building, and publishing.
pyproject.toml — Modern Python Project Config
What it is
pyproject.toml is the canonical configuration file for a modern Python project — replacing setup.py, setup.cfg, MANIFEST.in, and per-tool config files like .flake8 or mypy.ini. It is a TOML file that lives at the project root and declares three things: project metadata ([project], per PEP 621), the build system used to package the project ([build-system], per PEP 517/518), and tool-specific configuration ([tool.<name>], per PEP 518).
Maintained by the Python Packaging Authority, pyproject.toml is what pip install ., pip, uv, poetry, python -m build, ruff, mypy, and pytest all read. For new projects in 2026 it is the only config file you should be writing — the older setup.py and setup.cfg files are still supported but no longer recommended for new code.
Install
pyproject.toml is a file, not a tool — there is nothing to install. What you may need is a build frontend (build) and a build backend (chosen via [build-system]).
# Build frontend — invokes the backend declared in pyproject.toml
pip install build
# Twine — for uploading dists to PyPI
pip install twine
# A modern all-in-one (preferred)
pip install uv
Output: (none — exits 0 on success)
Minimal example
The smallest viable pyproject.toml declares a name, a version, and a build system. With just this, pip install . will package and install the project.
[project]
name = "my-tool"
version = "0.1.0"
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
Output: (none — exits 0 on success)
File anatomy
A full-featured pyproject.toml has three top-level table groups. Everything else ([tool.*]) is owned by individual tools.
[project] # PEP 621 — project metadata (name, version, deps, etc.)
[build-system] # PEP 517/518 — how to build a distribution
[tool.<name>] # tool-specific (ruff, mypy, pytest, hatch, poetry, …)
Output: (none — exits 0 on success)
[project] — core metadata
The [project] table is the PEP 621 declaration of who your project is. Most fields are optional; only name is strictly required, but in practice you should always set version (or mark it dynamic), description, readme, requires-python, and dependencies.
[project]
name = "my-tool"
version = "0.4.2"
description = "A delightful CLI for managing widgets"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
keywords = ["cli", "widgets", "automation"]
authors = [{ name = "Alice Dev", email = "alice@example.com" }]
maintainers = [{ name = "Alice Dev", email = "alice@example.com" }]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Utilities",
]
dependencies = [
"click>=8.1",
"rich>=13",
"httpx>=0.27",
]
Output: (none — exits 0 on success)
Field reference
| Field | Type | Purpose |
|---|---|---|
name | str (required) | distribution name on PyPI |
version | str | semver-style version (or list it under dynamic) |
description | str | one-line summary shown by pip show and PyPI |
readme | str or table | path to README, or { file = "README.md", content-type = "text/markdown" } |
requires-python | str | PEP 440 version specifier (e.g. ">=3.10,<4") |
license | str or table | SPDX expression or { file = "LICENSE" } |
authors / maintainers | list of tables | { name = "...", email = "..." } |
keywords | list of str | PyPI search tags |
classifiers | list of str | PyPI classifier strings |
urls | table | Homepage, Documentation, Repository, Issues, etc. |
dependencies | list of str | PEP 508 version specs |
optional-dependencies | table of lists | extras (pip install pkg[extra]) |
scripts | table | name = "module:function" console entry points |
gui-scripts | table | same, but for GUI apps (no console window on Windows) |
entry-points | table | other entry-point groups (e.g. plugins) |
dynamic | list of str | which fields are computed at build time |
Dependencies
Dependencies use PEP 508 syntax — the same string format pip install accepts. The most useful operators:
| Specifier | Meaning |
|---|---|
pkg==1.2.3 | exact |
pkg>=1.2,<2 | range |
pkg~=1.2 | compatible release (>=1.2,<2) |
pkg>=1.2; python_version<"3.12" | environment marker |
pkg[extra1,extra2]>=1.0 | with extras |
pkg @ git+https://github.com/o/r.git@v1 | direct URL |
[project]
dependencies = [
"click>=8.1",
"rich>=13",
"httpx[http2]>=0.27",
"tomli>=2.0; python_version<'3.11'",
"mylib @ git+https://github.com/alicedev/mylib.git@v1.2",
]
Output: (none — exits 0 on success)
Optional dependencies (extras)
Use [project.optional-dependencies] to define named groups of optional deps. Users opt in with pip install my-tool[group]. This is how packages like fastapi[all] and uvicorn[standard] work.
[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.4", "mypy>=1.10"]
docs = ["sphinx>=7", "furo"]
all = ["my-tool[dev,docs]"]
Output: (none — exits 0 on success)
PEP 735 (Python 3.13 standard) introduces a
[dependency-groups]table that is not shipped to PyPI — use it for development-only groups likelintortest. Tooling support (uv,pip 25+) is still maturing in 2026.
Scripts (console entry points)
[project.scripts] declares command-line entry points. Each name = "module:function" entry produces a launcher binary on pip install that calls the named function — no shebang scripts required.
[project.scripts]
my-tool = "my_tool.cli:main"
my-tool-dump = "my_tool.cli:dump"
Output: (none — exits 0 on success)
After pip install ., my-tool and my-tool-dump become available on $PATH.
my-tool --help
Output:
Usage: my-tool [OPTIONS] COMMAND [ARGS]...
A delightful CLI for managing widgets.
Options:
--help Show this message and exit.
URLs
[project.urls] is a free-form table of named URLs. PyPI renders these on the project page sidebar; common keys are shown below but any string label works.
[project.urls]
Homepage = "https://github.com/alicedev/my-tool"
Documentation = "https://my-tool.readthedocs.io"
Repository = "https://github.com/alicedev/my-tool"
Issues = "https://github.com/alicedev/my-tool/issues"
Changelog = "https://github.com/alicedev/my-tool/blob/main/CHANGELOG.md"
Output: (none — exits 0 on success)
Dynamic fields
dynamic = ["..."] declares fields that are computed by the build backend rather than written literally in pyproject.toml. The most common use is version — read from __init__.py, a git tag, or a separate VERSION file.
[project]
name = "my-tool"
dynamic = ["version"] # version comes from elsewhere
[tool.setuptools.dynamic]
version = { attr = "my_tool.__version__" }
Output: (none — exits 0 on success)
[build-system] — backend selection
[build-system] tells pip, build, and uv how to package your project. The two keys are requires (build-time dependencies) and build-backend (the dotted import path of the backend's build_meta). Every modern Python project should set this — without it, tools fall back to a legacy setup.py mode.
[build-system]
requires = ["setuptools>=68", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
Output: (none — exits 0 on success)
Build backend comparison
The five most common backends, in decreasing order of "batteries included":
| Backend | Install | build-backend | Notes |
|---|---|---|---|
| setuptools | bundled with pip | setuptools.build_meta | the legacy default; still the most compatible; supports C extensions out of the box |
| hatchling | pip install hatchling | hatchling.build | maintained by PyPA; fast, modern, no setup.py; powers hatch |
| flit-core | pip install flit-core | flit_core.buildapi | minimalist — pure-Python projects only, no C extensions |
| poetry-core | pip install poetry-core | poetry.core.masonry.api | for projects managed by poetry; also usable standalone |
| pdm-backend | pip install pdm-backend | pdm.backend | for projects managed by pdm; PEP 621-native |
| maturin | pip install maturin | maturin | Rust-extension Python packages |
| scikit-build-core | pip install scikit-build-core | scikit_build_core.build | CMake-based C/C++ extensions |
setuptools
The historical default. Use it for projects with C extensions, legacy setup.py history, or maximum tool compatibility.
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
"my_tool" = ["py.typed", "*.json"]
Output: (none — exits 0 on success)
hatchling
PyPA's modern recommendation for new pure-Python projects. Faster than setuptools, no setup.py, sensible defaults. Powers hatch but is usable standalone with any build frontend.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/my_tool"]
[tool.hatch.version]
path = "src/my_tool/__init__.py"
Output: (none — exits 0 on success)
flit-core
Pure-Python projects with no C code, no plugins, no fuss. Reads __version__ from your top-level package automatically.
[build-system]
requires = ["flit_core>=3.9"]
build-backend = "flit_core.buildapi"
[project]
name = "my-tool"
dynamic = ["version", "description"]
Output: (none — exits 0 on success)
poetry-core
For projects managed by poetry. Older poetry projects also use [tool.poetry] for metadata; the modern (poetry 2.0+) layout is fully PEP 621 with [project].
[build-system]
requires = ["poetry-core>=2.0"]
build-backend = "poetry.core.masonry.api"
[project]
name = "my-tool"
version = "0.1.0"
dependencies = ["click>=8"]
Output: (none — exits 0 on success)
pdm-backend
For projects managed by pdm. Like hatchling, it's modern and PEP 621-first; pick it if your team is on PDM.
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
Output: (none — exits 0 on success)
Common [tool.*] tables
Most Python tools read their config from [tool.<name>] so you can consolidate everything into one file. The most common settings below.
[tool.ruff]
ruff (and ruff format) replaces flake8, isort, pyupgrade, black, and a dozen others. The settings nest under [tool.ruff] with sub-tables for lint and format.
[tool.ruff]
line-length = 100
target-version = "py310"
src = ["src", "tests"]
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM", "RUF"]
ignore = ["E501"] # line-length handled by formatter
[tool.ruff.format]
quote-style = "double"
Output: (none — exits 0 on success)
[tool.mypy]
mypy reads [tool.mypy] and per-module overrides under [[tool.mypy.overrides]]. The strict = true shortcut enables every recommended check.
[tool.mypy]
python_version = "3.12"
strict = true
warn_unused_configs = true
files = ["src", "tests"]
[[tool.mypy.overrides]]
module = ["legacy.*"]
ignore_errors = true
[[tool.mypy.overrides]]
module = ["yaml.*", "requests.*"]
ignore_missing_imports = true
Output: (none — exits 0 on success)
[tool.pytest.ini_options]
The pytest table is awkwardly named ini_options for historical reasons — when pytest reads pyproject.toml it expects everything under that key.
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
addopts = "-ra -q --strict-markers --cov=src"
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks integration tests",
]
filterwarnings = [
"error",
"ignore::DeprecationWarning:pkg_resources",
]
Output: (none — exits 0 on success)
[tool.coverage]
coverage.py reads [tool.coverage.run] and [tool.coverage.report]. Pair with pytest-cov's --cov flag.
[tool.coverage.run]
branch = true
source = ["src"]
[tool.coverage.report]
show_missing = true
skip_covered = true
fail_under = 80
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"if TYPE_CHECKING:",
]
Output: (none — exits 0 on success)
[tool.uv]
uv reads [project] for dependencies (PEP 621) and its own [tool.uv] for workspace, sources, and dev groups.
[tool.uv]
dev-dependencies = ["pytest>=8", "ruff>=0.4", "mypy>=1.10"]
[tool.uv.sources]
mylib = { git = "https://github.com/alicedev/mylib.git", branch = "main" }
Output: (none — exits 0 on success)
[tool.hatch]
When using hatchling as a backend with the hatch CLI, [tool.hatch.envs.*] defines reproducible scripted environments.
[tool.hatch.envs.default]
dependencies = ["pytest", "pytest-cov"]
[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
test-cov = "pytest --cov=src {args:tests}"
lint = "ruff check src tests"
Output: (none — exits 0 on success)
[tool.black] (if still used)
Most teams have migrated to ruff format, but black is still common. Note line-length defaults to 88.
[tool.black]
line-length = 100
target-version = ["py310", "py311", "py312"]
Output: (none — exits 0 on success)
A complete example
A real-world pyproject for a small CLI tool published to PyPI, with optional dev/test groups, linting, type-checking, and test config.
[project]
name = "widgets-cli"
version = "1.0.0"
description = "Manage widgets from the command line"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [{ name = "Alice Dev", email = "alice@example.com" }]
keywords = ["cli", "widgets"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3 :: Only",
"License :: OSI Approved :: MIT License",
]
dependencies = [
"click>=8.1",
"rich>=13",
"httpx>=0.27",
"pydantic>=2.7",
"tomli>=2.0; python_version<'3.11'",
]
[project.optional-dependencies]
dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.4", "mypy>=1.10"]
docs = ["sphinx>=7", "furo"]
[project.scripts]
widgets = "widgets_cli.__main__:main"
[project.urls]
Homepage = "https://github.com/alicedev/widgets-cli"
Documentation = "https://widgets-cli.readthedocs.io"
Repository = "https://github.com/alicedev/widgets-cli"
Issues = "https://github.com/alicedev/widgets-cli/issues"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/widgets_cli"]
[tool.ruff]
line-length = 100
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM"]
[tool.mypy]
python_version = "3.12"
strict = true
files = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra -q --cov=src"
[tool.coverage.report]
fail_under = 80
Output: (none — exits 0 on success)
Building a distribution
python -m build reads [build-system], installs the listed requirements into an isolated venv, then invokes the backend to produce a source distribution (sdist) and a built distribution (wheel).
# Install the build frontend once
pip install build
# Produce dist/widgets_cli-1.0.0.tar.gz and dist/widgets_cli-1.0.0-py3-none-any.whl
python -m build
ls dist/
Output:
* Creating isolated environment: venv+pip...
* Installing packages in isolated environment:
- hatchling
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Successfully built widgets_cli-1.0.0.tar.gz and widgets_cli-1.0.0-py3-none-any.whl
widgets_cli-1.0.0-py3-none-any.whl
widgets_cli-1.0.0.tar.gz
uv buildandhatch buildare drop-in replacements forpython -m buildwith the same output structure but faster isolated-env setup.
Publishing to PyPI
twine upload uploads the built artifacts to PyPI (or TestPyPI). Authenticate with an API token stored in ~/.pypirc or the TWINE_PASSWORD environment variable.
# Test against TestPyPI first
twine upload --repository testpypi dist/*
# Real upload
twine upload dist/*
Output:
Uploading distributions to https://upload.pypi.org/legacy/
Uploading widgets_cli-1.0.0-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 24.3/24.3 kB • 00:01 • ?
Uploading widgets_cli-1.0.0.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 18.7/18.7 kB • 00:01 • ?
View at: https://pypi.org/project/widgets-cli/1.0.0/
Once a version is uploaded to PyPI you cannot replace it — only yank it. Always test on TestPyPI first, and bump the version on every retry.
setup.py → pyproject.toml migration
The most common migration is from the classic setup.py (or setup.cfg) to a pyproject.toml-only project. Map fields as follows:
setup.py | pyproject.toml |
|---|---|
name= | [project] name |
version= | [project] version or dynamic = ["version"] |
description= | [project] description |
long_description=open("README.md").read() | [project] readme = "README.md" |
long_description_content_type=... | infer from readme extension |
author=, author_email= | [project] authors = [{name, email}] |
python_requires=">=3.10" | [project] requires-python = ">=3.10" |
install_requires=[...] | [project] dependencies = [...] |
extras_require={...} | [project.optional-dependencies] |
entry_points={"console_scripts": [...]} | [project.scripts] |
packages=find_packages(...) | [tool.setuptools.packages.find] |
package_data={...} | [tool.setuptools.package-data] |
include_package_data=True | [tool.setuptools] include-package-data = true |
classifiers=[...] | [project] classifiers = [...] |
keywords=[...] | [project] keywords = [...] |
license=... | [project] license = { text = "..." } |
Before — setup.py
from setuptools import setup, find_packages
setup(
name="my-tool",
version="0.4.2",
description="A delightful CLI for managing widgets",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
author="Alice Dev",
author_email="alice@example.com",
python_requires=">=3.10",
packages=find_packages(where="src"),
package_dir={"": "src"},
install_requires=["click>=8.1", "rich>=13"],
extras_require={"dev": ["pytest", "ruff"]},
entry_points={"console_scripts": ["my-tool=my_tool.cli:main"]},
)
Output: (none — exits 0 on success)
After — pyproject.toml
[project]
name = "my-tool"
version = "0.4.2"
description = "A delightful CLI for managing widgets"
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Alice Dev", email = "alice@example.com" }]
dependencies = ["click>=8.1", "rich>=13"]
[project.optional-dependencies]
dev = ["pytest", "ruff"]
[project.scripts]
my-tool = "my_tool.cli:main"
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
Output: (none — exits 0 on success)
Reading pyproject.toml from Python
Python 3.11+ ships a tomllib module in the standard library — no third-party dep needed.
import sys
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
with open('pyproject.toml', 'rb') as f:
data = tomllib.load(f)
print(data['project']['name'])
print(data['project']['dependencies'])
Output:
widgets-cli
['click>=8.1', 'rich>=13', 'httpx>=0.27', 'pydantic>=2.7', "tomli>=2.0; python_version<'3.11'"]
tomllib.loadrequires the file be opened in binary mode ('rb'), not text mode. Usetomllib.loads(string)if you already have the contents as a string.
Common pitfalls
- Missing
[build-system]— without it,pip install .falls back to a legacy mode that may not respect your declared metadata. Always include it, even for trivial projects. - Mixing
[tool.poetry]and[project]— older poetry projects used[tool.poetry]exclusively. Poetry 2.0+ supports PEP 621[project]. Don't define the same field in both — only one wins, depending on the version. - Forgetting
requires-python— without it,pipwill let users install your package on incompatible interpreters and fail at runtime. - Wrong
build-backendimport path —setuptools.build_meta(with underscore), notsetuptools.build-meta. The backend name follows Python module syntax. dynamicfield also defined statically — if you listversionindynamic, you must NOT also writeversion = "..."in[project]. The build will fail.- TOML escaping in strings —
\ninside a"..."string is a newline; use single-quoted'...'literal strings to disable escaping (e.g. Windows paths). - Tool config in the wrong section —
[ruff]does nothing; it must be[tool.ruff]. Same for every other tool table. - Comma in TOML arrays after the last item — trailing commas are allowed in arrays and inline tables. Missing commas between array elements are not — TOML errors are surfaced by
python -m buildlong before pip touches it. license = "MIT"is not valid (license must be a table or use the SPDX-expression formlicense = "MIT"only after PEP 639 became standard in Python 3.13). For maximum compatibility uselicense = { text = "MIT" }orlicense = { file = "LICENSE" }.- Editable installs with src-layout —
pip install -e .requires eithersetuptools>=64with[tool.setuptools.packages.find]configured, or hatchling/pdm-backend which handle src-layout natively.
Real-world recipes
Library with C extension
A package shipping a C extension module, built via setuptools.
[project]
name = "myfast"
version = "0.1.0"
requires-python = ">=3.10"
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["myfast"]
[tool.setuptools.package-data]
myfast = ["*.pyi", "py.typed"]
Output: (none — exits 0 on success)
Pair with a minimal setup.py for the extension itself:
from setuptools import Extension, setup
setup(
ext_modules=[
Extension(
"myfast._core",
sources=["src/myfast/_core.c"],
extra_compile_args=["-O2"],
),
],
)
Output: (none — exits 0 on success)
Pure-Python library with flit-core
For a one-file or single-package pure-Python library, flit-core is the simplest backend on the market.
[project]
name = "tiny-tool"
authors = [{ name = "Alice Dev", email = "alice@example.com" }]
dynamic = ["version", "description"]
[build-system]
requires = ["flit_core>=3.9"]
build-backend = "flit_core.buildapi"
Output: (none — exits 0 on success)
Add a one-line docstring and __version__ to your package and flit infers the metadata:
"""A tiny tool that does one thing well."""
__version__ = "0.1.0"
Output: (none — exits 0 on success)
Monorepo with workspaces (uv)
uv supports a workspace layout — one top-level pyproject.toml plus per-package pyproject.toml files sharing one lockfile.
[project]
name = "myapp"
version = "0.1.0"
[tool.uv.workspace]
members = ["packages/*"]
[tool.uv.sources]
shared = { workspace = true }
[project.dependencies]
shared = "*"
Output: (none — exits 0 on success)
Dynamic version from git tags
Read the version directly from your most recent git tag with setuptools-scm. The version field is left out of [project] and added to dynamic.
[project]
name = "my-tool"
dynamic = ["version"]
[build-system]
requires = ["setuptools>=68", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
write_to = "src/my_tool/_version.py"
Output: (none — exits 0 on success)
After git tag v1.2.3, python -m build produces my_tool-1.2.3-py3-none-any.whl.
CLI plugin via entry-points
Expose a plugin discovery hook for third-party packages to register.
[project.entry-points."widgets_cli.plugins"]
widget-stats = "my_plugin.stats:register"
widget-export = "my_plugin.export:register"
Output: (none — exits 0 on success)
Plugin discovery from Python (3.10+):
from importlib.metadata import entry_points
for ep in entry_points(group='widgets_cli.plugins'):
handler = ep.load()
handler()
Output:
registered: widget-stats
registered: widget-export
Single source of truth for tool configs
Consolidate ruff, mypy, pytest, coverage, and uv into one file — replacing .flake8, mypy.ini, pytest.ini, .coveragerc, and setup.cfg simultaneously.
[tool.ruff]
line-length = 100
[tool.mypy]
strict = true
[tool.pytest.ini_options]
addopts = "-ra -q"
[tool.coverage.run]
source = ["src"]
[tool.uv]
dev-dependencies = ["pytest", "ruff", "mypy"]
Output: (none — exits 0 on success)
CI-ready build + publish workflow
A GitHub Actions snippet that builds on every tag push and publishes to PyPI via trusted publishing (no token in CI).
name: publish
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write # for PyPI trusted publishing
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install build
- run: python -m build
- uses: pypa/gh-action-pypi-publish@release/v1
Output: (none — exits 0 on success)
See also
- pip — installs from
pyproject.tomlviapip install . - uv — fastest tool to manage
pyproject.tomlprojects in 2026 - poetry — alternative project manager that owns its own
pyproject.tomlflavor - ruff, mypy, pytest — common
[tool.*]consumers - PEP 621 —
[project]metadata spec - PEP 517, PEP 518 —
[build-system]spec