If you learned Python a few years ago, packaging meant a confusing pile: setup.py, setup.cfg, requirements.txt, MANIFEST.in. Today one file replaces most of that — pyproject.toml. Here is what it is and why it exists.
The Problem It Solves
The old setup.py was a Python script that ran to describe your package. That created a chicken-and-egg problem: tools had to execute arbitrary code just to learn what your package needed. It was also non-standard — every tool invented its own config format.
pyproject.toml is declarative data, not code. Any tool can read it without running anything.
The Three Sections That Matter
[build-system] tells installers how to build your package. This is the part that ended the chicken-and-egg problem:
[build-system] requires = ["hatchling"] build-backend = "hatchling.build"
[project] is the standardized metadata — name, version, dependencies. This replaced setup.py's arguments:
[project] name = "myapp" version = "1.2.0" requires-python = ">=3.11" dependencies = [ "httpx>=0.27", "pydantic>=2.0", ]
[tool.*] is where individual tools keep their config, so they stop needing separate files:
[tool.ruff] line-length = 100 [tool.mypy] strict = true
Where requirements.txt Fits Now
dependencies in [project] lists the abstract requirements of your package ("needs httpx 0.27+"). A lock file — produced by tools like uv or pip-tools — pins the exact versions for reproducible installs. You still have both, but the roles are now clear: pyproject.toml declares, the lock file freezes.
Why It Won
- ▸One standard file every tool understands.
- ▸Declarative, so no code execution to read metadata.
- ▸Absorbs tool config, killing the
setup.cfg/MANIFEST.insprawl.
Summary
pyproject.toml is the single, standardized home for your project's build instructions, metadata, and tool settings. Learn its three core tables — [build-system], [project], and [tool.*] — and the historical packaging confusion mostly disappears.