The Python packaging ecosystem continues to thrive – over 158,000 packages are available on PyPI as of January 2023. But with great success comes great complexity. Challenges abound in areas like dependency management, build configuration and environment reproducibility.
Cue pyproject.toml – an initiative to create conventions through PEP 518 and help address these pain points within the Python world.
In this comprehensive guide, we dive deep on everything pyproject.toml has to offer:
- Its background, purpose and key motivations
- How it enables flexibility and standardization
- Contents, syntax and specifications
- Packaging tutorials using various build tools
- Best practices for optimal use
Let‘s get right to it!
Overview
First, some background. As Python packaging grew more complex over the past decade with tools like setuptools, distutils, pip and poetry – limitations around interoperability and reproducibility became apparent. Core areas that were impacted:
- Building packages in a consistent wheel (.whl) format
- Dependency management and environment installation
- Metadata and configuration sharing between packaging tools
This caused challenges with reliably building code across different systems. pyproject.toml came out of the need to establish conventions and improve the status quo.
The key goals behind standardizing pyproject.toml are:
- Flexibility: Support multiple build tools like poetry, flit, pip while outputting consistent packages
- Interoperability: Common metadata format for tools to consume configuration and share data
- Reproducibility: Reliably reproduce build environments across different systems
- Simplicity: Easy to use configuration format for defining projects
This guide covers various facets of pyproject.toml in depth:
- Configuration format, syntax and contents
- Interoperability between build systems
- Packaging tutorial using setuptools
- Best practices for optimal usage
So let‘s explore the innards of this ubiquitous new standard!
What is pyproject.toml and How Does it Work?
pyproject.toml is a configuration file that contains metadata and defines how a Python project should be built and packaged.
History
Problems with Python build tools motivated the need for standardization. For example:
- setup.py scripts became complex and error-prone
- Dependency management through multiple files like requirements.txt
- Lack of consistency across distribution formats like wheels, eggs, sdists
To address this, PEP 518 proposed conventions that were ratified as the pyproject.toml spec. It was first available in Python 3.6+.
Some key functionality include:
- Declarative config using TOML syntax instead of imperative build scripts
- Ability to support multiple build backends – setuptools, poetry, flit etc
- Dependency declaration and environment reproducibility
- Metadata like name, version – required by packaging systems
- Tool configuration section for linters, formatters etc
In a nutshell, pyproject.toml handles critical aspects of Python packaging in a standard manner.
Contents
pyproject.toml uses TOML syntax – a minimal configuration language intended for clarity and readability.
It contains various sections like:
[build-system]
Defines how to build the project:
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
Contains metadata like name, description, authors etc:
[project]
name = "myproject"
version = "1.0"
description = "A fun package"
authors = [
{ name="Alice"},
{ name="Bob" }
]
[tool.X]
Configures tools like linters and formatters:
[tool.black]
line-length = 100
[metadata]
Additional metadata like Python version required, dependencies etc.
[metadata]
requires-python = ">=3.6"
dependencies = ["requests", "fastapi"]
As we can see, pyproject.toml provides a wide range of configuration – from build specification to tooling setup!
Build Systems and Interoperability
A key focus of pyproject.toml is improving interoperability between Python build systems and packaging tools. Let‘s explore this aspect.
Overview
Python has an extensive ecosystem of build tools and distribution formats including:
- Packaging: setuptools, poetry, flit, hatch, pip
- Formats: wheels(.whl), eggs, sdists
However, limitations around consistency meant:
- Difficulty switching between tools like setuptools to poetry
- Inability to reliably produce wheels across environments
pyproject.toml establishes common standards to fix this through:
- Unified build configuration and metadata
- Requirement to output wheels and sdists
- Support for multiple build backends
This improves flexibility, interoperability and reproducibility.
Standardization Examples
Some examples where pyproject.toml enables standardization:
Producing wheels
The [build-system]
section ensures any compliant backend can output .whl
files:
[build-system]
# Build wheel packages
build-backend = "setuptools.build_meta"
Tool configuration
Tools can read config under [tool.X]
:
[tool.pylint]
max-line-length = 100
[tool.pytest]
addopts = "-ra -q"
This allows easy toolchain integration regardless of build backend used.
Switching backends
Project metadata and dependencies are specified in a common format – allowing switching build tools:
[project]
# Name, version applies to any build tool
name = "package"
version = "1.0"
[metadata]
requires-python = ">=3.6"
So in summary, pyproject.toml establishes conventions that connect disparate tools to improve interoperability.
Packaging Python Projects Tutorial
Let‘s see how to leverage pyproject.toml for packaging projects using the setuptools build backend.
Project scaffolding
We will use the following folder structure:
package
├─ src
│ ├─ package
│ ├─ __init__.py
| ├─ main.py
├─ pyproject.toml
├─ README.md
Where:
src
contains our Python codepyproject.toml
defines the buildREADME
has documentation
Minimal pyproject.toml
Here is a minimal pyproject.toml:
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "package"
version = "1.0.0"
description = "My package"
authors = [
{ name="Alice" },
{ name="Bob" }
]
[tool.setuptools]
packages = ["package"]
We specify setuptools as the build backend along with basic metadata.
The [tool.setuptools]
section tells setuptools our Python code to package is in the package
folder.
Running the build
To build a wheel run:
python -m build
After the build completes, a dist
folder with the *.whl package is created!
We can upload this file to PyPI for others to install our package using pip
.
Additional customization
Additional things we can customize include:
- Declaring dependencies
- Adding package entry points
- Supporting plugins
- Creating platform specific wheels
And much more!
So in summary, by leveraging pyproject.toml and PEP 518 standards, we can package Python code with simplicity and consistency.
Recommendations and Best Practices
Let‘s round up the guide by going over some best practices when using pyproject.toml:
Use over setup.py
Prefer pyproject.toml over setup.py for defining builds as it‘s the newer standard.
Declare dependencies
Accurately specify dependencies in [project]/metadata
sections to improve reproducibility:
[metadata]
dependencies = [
"requests >=2.28.1,<3",
"fastapi >=0.88,<0.89"
]
Use appropriate version specifiers.
Configure tools
Make use of [tool.]
sections to integrate linters, formatters etc:
[tool.isort]
line_length = 100
[tool.pyright]
reportMissingTypeStubs = true
This avoids need for separate configuration files.
Reproducibility
Take care to freeze dependencies and isolate build environment for reproducible builds across systems.
For detailed tips, refer to the official packaging guide.
Transitioning
When migrating existing projects to pyproject.toml, reuse metadata from setup.py and requirements.txt files.
Also reference the porting guide by setuptools.
So in summary:
- Favor pyproject.toml over older standards
- Declare dependencies carefully with correct versions
- Integrate tools into unified config approach
- Take steps to improve reproducibility
This will lead to easier and more robust Python packaging!
The Road Ahead
The Python ecosystem continues to rapidly adopt pyproject.toml as the standard for project configuration – a promising trend for improving interoperability.
But active maintenance is required to balance flexibility and convention as more build tools emerge. The working group behind PEP 518 has their work cut out!
While there are still areas of debate, I believe the core focus on reproducibility and support for multiple backends means pyproject.toml is here to stay. Exciting times ahead for Python packaging!