Part 3 of 8 · 3 min read
pyproject.toml
The one file that describes your project, read line by line, and the difference between a range and a lock file.
Dependency
A library your code needs in order to run. Direct dependencies are the ones you chose. Transitive dependencies are the libraries that your libraries need, which is why installing one package brought several.
A project needs one file that lists its dependencies and describes itself. In modern Python that file is pyproject.toml. This is the one from the example project triage-service in this module:
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "triage"
version = "0.1.0"
description = "A tiny, typed Python service skeleton."
requires-python = ">=3.11"
dependencies = ["pydantic>=2.7,<3"]
[project.optional-dependencies]
dev = ["pytest>=8", "mypy>=1.10"]
[project.scripts]
triage = "triage.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
[tool.mypy]
strict = true
[tool.pytest.ini_options]
testpaths = ["tests"]
TOML is a simple settings format. Names in square brackets are sections. Here is what each section does:
| Section or line | What it means |
|---|---|
[build-system] |
Which tool turns this folder into an installable package. Here it is setuptools. You rarely change this. |
name, version, description |
Who the project is. |
requires-python = ">=3.11" |
The project refuses to install on an older Python. |
dependencies |
Libraries needed to run the service. >=2.7,<3 means "version 2.7 or newer, but not version 3". That allows fixes and new features, and blocks the next major version, which may break your code. |
optional-dependencies / dev |
Tools that only developers need (tests and a type checker). You install them with pip install -e ".[dev]". |
[project.scripts] |
Makes a command called triage that runs the function main in the file triage/cli.py. |
[tool.setuptools.packages.find] |
Tells setuptools to look for packages inside the src folder. |
[tool.mypy], [tool.pytest...] |
Settings for other tools. They live in the same file so there is one place to look. |
Install the project and its developer tools with one command:
(.venv) $ pip install -e ".[dev]"
(.venv) $ pip list
Package Version
--------- -------
mypy 2.4.1
pydantic 2.14.2
pytest 9.2.0
triage 0.1.0 /home/asha/triage-service
...
The -e means editable: instead of copying your code into the environment, pip links to it. You edit a file and the change takes effect immediately. Your version numbers will be newer than these, which is normal.
A range, or an exact version?
The range pydantic>=2.7,<3 describes what your project accepts. That is right for the file that describes the project, so that other projects can use yours. But for the thing you actually deploy, you want to run the exact versions that passed your tests, not whatever was newest that morning.
Lock file
A list of the exact version of every package, direct and transitive, that was installed when your tests passed. You create one with pip freeze > requirements.lock (or uv lock), and you install from it when you deploy.
(.venv) $ pip freeze > requirements.lock
(.venv) $ head -3 requirements.lock
annotated-types==0.8.0
ast_serialize==0.13.1
iniconfig==2.3.1
The range says what is allowed. The lock records what was tested.
Check yourself
- What does
>=2.7,<3allow and forbid? - What does
pip install -edo differently frompip install? - Why keep both a range in
pyproject.tomland a lock file?