pyproject.toml is the contract behind modern Python dependency management.
Without it, installers have to execute setup.py or guess build requirements, which leads to fragile builds and non-reproducible environments.
This article breaks down the key PEPs (518/517/621) and how pyproject.toml connects to python -m venv + python -m pip for predictable installs.
The old problem: executable setup.py
Back then, project configuration was executable Python code. Flexible, yes, but it also enabled side effects and fragile builds.
Tooling could not reliably parse metadata statically.
The fix in three key PEPs
PEP 518: declare the build system
It defines [build-system] in pyproject.toml so installers know what they need before building.
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
PEP 517: standardized build backend interface
It lets pip work with multiple backends (setuptools, hatchling, flit, poetry-core) through a common API.
PEP 621: project metadata in [project]
It standardizes name, version, dependencies, authors, and more without dynamic code.
Modern pyproject.toml example
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-service"
version = "0.1.0"
description = "Internal API example"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.110",
"uvicorn[standard]>=0.29",
"pydantic>=2.7"
]
[project.optional-dependencies]
dev = [
"pytest>=8",
"ruff>=0.5",
"mypy>=1.10"
]
This improves three critical areas:
- project clarity
- reproducible installation
- sustainable dependency management
How to use it with python venv
python -m venv .venv
source .venv/bin/activate
python -m pip install -e .
python -m pip install -e ".[dev]"
In editable mode, source code changes are reflected without reinstalling every time.
Common mistakes
- Leaving out
[build-system](or listing the wrong backend), causing builds to fail or behave differently across machines. - Duplicating dependency declarations across
requirements.txtandpyproject.tomlwith no “source of truth,” leading to drift. - Not setting
requires-python, so installs succeed on incompatible interpreters and fail later at runtime. - Treating version ranges as “good enough” without a lock file, then getting different dependency graphs in CI vs local.
venv vs virtualenv vs poetry (practical view)
Quick team-level view:
- venv: simple and standard, great to start
- virtualenv: more options and speed in some scenarios
- poetry: integrated workflow for dependencies and publishing
All of them can coexist with pyproject.toml, which is now the common language of Python packaging.
Insight that often saves hours
If an environment starts failing “out of nowhere” after many package experiments, your code may not be the problem; environment state often is.
In many cases, recreating .venv from pyproject.toml is faster than patching inconsistent installs one by one.
Environment hygiene pro tip
Dependency experiments tend to leave behind throwaway environments that later confuse installs and imports. If that keeps happening, KillPy can scan and list stale environments by age/size so you can remove them after confirming they are unused.
Conclusion
pyproject.toml is not a trend. It is the modern foundation of Python environments and dependency management.
If your goal is real reproducibility, less team friction, and predictable builds, this file should be at the center of your workflow.
Has your project fully migrated to pyproject.toml, or are you still transitioning from setup.py? Share your case in the comments; it can help other teams.
Top comments (0)