pyproject.toml Explained
18 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)A package must say its name, its version and what it needs. pyproject.toml is the one file where all of that lives. It is plain text, and Python can read it with the standard library.
One file for the whole project
pyproject.toml is the standard project file. It names the build tool, describes the package, lists its dependencies, and holds settings for other tools.
Older projects spread settings over setup.py, setup.cfg and more. A single pyproject.toml now holds them all. It uses TOML, a plain text format with [tables] and key = value lines.
[project]name = "shopkit"version = "0.1.0"pip, build tools, mypy and pytest all read this one file.
The build-system table
Build backend is the tool that turns your source folder into an installable file. The [build-system] table tells pip which one to download and call.
This table tells pip which tool builds your package. pip downloads the tools in requires and calls the build-backend. setuptools is common, and hatchling and flit are lighter choices.
[build-system]requires = ["setuptools>=68"]build-backend = "setuptools.build_meta"Without this table, pip falls back to old defaults. Always include it.
The project table
This table describes the package. name is what users type after pip install. version is the release number. requires-python states which Python versions are supported.
[project]name = "shopkit"version = "0.1.0"description = "Small helpers for shop calculations"requires-python = ">=3.10"dependencies = ["python-dateutil>=2.8"]Example 1 · Reading the project table
import tomllib
text = """[build-system]requires = ["setuptools>=68"]build-backend = "setuptools.build_meta"
[project]name = "shopkit"version = "0.1.0"description = "Small helpers for shop calculations"requires-python = ">=3.10"dependencies = ["python-dateutil>=2.8"]
[project.optional-dependencies]dev = ["mypy", "pytest"]
[project.scripts]shopkit-total = "shopkit.__main__:main""""
config = tomllib.loads(text)print(config["project"]["name"], config["project"]["version"])print(config["build-system"]["build-backend"])shopkit 0.1.0 setuptools.build_meta
What this code does
tomllibis in the standard library since Python 3.11.loadsturns TOML text into dictionaries.- Each
[table]becomes a key, and dotted names such as[project.scripts]become nested dictionaries.
Example 2 · Dependencies and the Python version
import tomllib
text = """[build-system]requires = ["setuptools>=68"]build-backend = "setuptools.build_meta"
[project]name = "shopkit"version = "0.1.0"description = "Small helpers for shop calculations"requires-python = ">=3.10"dependencies = ["python-dateutil>=2.8"]
[project.optional-dependencies]dev = ["mypy", "pytest"]
[project.scripts]shopkit-total = "shopkit.__main__:main""""
config = tomllib.loads(text)project = config["project"]print(project["requires-python"])for requirement in project["dependencies"]: print(requirement)>=3.10 python-dateutil>=2.8
What this code does
requires-pythonis one string.dependenciesis a list of strings.- Each requirement is a package name with an optional version rule.
dependencies lists the packages needed to run the code. Write ranges, not exact pins, so that your package fits with other people's projects.
Optional groups and scripts
Optional dependencies are named groups of extra packages, such as dev or docs. They install only when asked for, with pip install ".[dev]".
Tools such as mypy and pytest are needed to develop, not to run. Put them in a named group. A [project.scripts] entry gives users a command that calls one of your functions.
[project.optional-dependencies]dev = ["mypy", "pytest"][project.scripts]shopkit-total = "shopkit.__main__:main"Example 3 · Groups and scripts
import tomllib
text = """[build-system]requires = ["setuptools>=68"]build-backend = "setuptools.build_meta"
[project]name = "shopkit"version = "0.1.0"description = "Small helpers for shop calculations"requires-python = ">=3.10"dependencies = ["python-dateutil>=2.8"]
[project.optional-dependencies]dev = ["mypy", "pytest"]
[project.scripts]shopkit-total = "shopkit.__main__:main""""
config = tomllib.loads(text)project = config["project"]print(project["optional-dependencies"]["dev"])print(project["scripts"])['mypy', 'pytest']
{'shopkit-total': 'shopkit.__main__:main'}What this code does
- The
devgroup is just another list of requirements. - A script maps a command name to
module:function.
Example 4 · The console script created by pip
Runs in: Your terminal
shopkit-total2360.0
What this code does
- After
pip install -e ., pip writes a small launcher namedshopkit-totalinto the environment. - It calls
maininshopkit/__main__.py, so users run your code without typingpython -m.
Install the group with pip install -e ".[dev]". The script line says: create the command shopkit-total and call main from shopkit/__main__.py.
Settings for other tools
Each tool keeps its settings in a table named [tool.<name>]. The type checker from the last module is configured here. Your project then has one place for its rules.
[tool.mypy]python_version = "3.12"strict = true[tool.pytest.ini_options]testpaths = ["tests"]A new teammate clones the project, installs it and runs the same checks with the same settings.
Next step
Check your understanding
8 quick questions