C.W.K.
Stream
Lesson 04 of 05 · published

pyproject.toml — The Modern Project File

~18 min · pyproject, build-system, metadata, pip

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

The single source of truth

pyproject.toml (PEP 517 / 518 / 621) is the modern Python project file. It replaces setup.py, setup.cfg, and a dozen tool-specific config files. 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

In [project.dependencies], list runtime dependencies. 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.

Pythonic Way: Every Python project gets a pyproject.toml, even small ones. It's the canonical place for tool configuration (ruff/mypy/pytest), and every modern tool reads it. Setup.py is deprecated for new projects.

Code

A minimal pyproject.toml·toml
# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-package"
version = "0.1.0"
description = "A small package"
readme = "README.md"
requires-python = ">=3.10"
authors = [{name = "Pippa", email = "pippa@example.com"}]
dependencies = [
    "requests>=2.31",
    "click>=8.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "ruff>=0.1",
    "mypy>=1.0",
]

[project.scripts]
my-cli = "my_package.cli:main"
Tool sections — config in one place·toml
# pyproject.toml (continued)
[tool.ruff]
line-length = 100
select = ["E", "F", "W", "I", "UP"]

[tool.mypy]
python_version = "3.10"
strict = true
ignore_missing_imports = true

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-v --tb=short"
Installing your project·bash
# Editable install (development)
pip install -e .

# Editable + dev dependencies
pip install -e ".[dev]"

# Build a wheel for distribution
pip install build
python -m build           # creates dist/my-package-0.1.0-*.whl

# Upload to PyPI (when you're ready to publish)
# pip install twine
# twine upload dist/*
Reading project metadata at runtime·python
# 3.8+ — importlib.metadata
from importlib.metadata import version, metadata

print(version("requests"))             # '2.31.0'
m = metadata("requests")
print(m["Name"])
print(m["Version"])
print(m["Summary"])

# Useful for: showing your own package version, checking installed deps

External links

Exercise

Create a tiny package directory /tmp/mini_pkg/ with: pyproject.toml (using hatchling as build backend, name 'mini-pkg', version '0.1', a single dependency on 'click'), mini_pkg/__init__.py (defines __version__ = '0.1'), mini_pkg/cli.py (a click command that prints 'hi'). Run pip install -e /tmp/mini_pkg/ in a venv and confirm from mini_pkg import __version__ works.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.