The single source of truth
pyproject.toml (PEP 517 / 518 / 621) is the modern Python project file. It standardizes build-system and project metadata, and many tools can also place configuration there. Existing setup.py and setup.cfg projects still exist. Inside it: build system, package metadata (name, version, dependencies), and tool configurations (ruff, mypy, pytest, etc.).
The two essential sections
[build-system] tells pip how to build your package — the build backend (e.g., hatchling, setuptools, poetry-core). [project] contains the metadata: name, version, description, dependencies, Python version requirement, license. Tool sections ([tool.ruff], [tool.mypy], etc.) are optional.
Dependencies — pinned vs unpinned
Under [project], list runtime requirements in a dependencies = [...] array. Pin loosely (requests >= 2.31) for libraries — over-pinning makes you incompatible with everyone. Pin tightly (requests == 2.31.0) for applications — you want reproducible deploys. [project.optional-dependencies.dev] for dev-only deps (pytest, ruff).
Editable installs — the dev pattern
pip install -e . (or uv pip install -e .) installs your package in "editable" mode — Python imports from your source directory, so edits show up immediately without re-installing. Indispensable when developing a package.
pyproject.toml, even small ones. It's the canonical place for tool configuration (ruff/mypy/pytest), and many modern tools read it. Start new projects with pyproject.toml; avoid deprecated direct commands such as python setup.py install. The mere presence of a setup.py file is not universally deprecated.
Libraries and applications need different dependency policies. A library that exact-pins every transitive package becomes hard to compose; an application with loose ranges loses deployment reproducibility. Declared compatibility and a resolved lock file carry different responsibilities.