今更聞けない pyproject.toml の読み方
はじめに
私は普段 TypeScript(Next.js / NestJS)を使用しているので、package.json を見慣れています。
最近、AI Agent の実装をしてみようと考え、Python プロジェクトを uv(パッケージマネージャー)で立ち上げました。
すると、package.json の代わりに pyproject.toml というファイルが生成されました。
最初はまったく読み方がわかりませんでしたが、調べてみると package.json と対応する部分が多く、TypeScript エンジニアにとっても理解しやすい構造になっていました。
私なりに、pyproject.toml を package.json で例えながらまとめてみました。
今回面白いと思ったのは、4. [tool.*] — 各種開発ツールの設定の部分です。
参照資料
本記事は以下の公式ドキュメントを参照しています。
- pyproject.toml specification - Python Packaging User Guide (PEP 518 / PEP 621 定義)
- Writing your pyproject.toml - Python Packaging User Guide
- PEP 518 – Specifying Minimum Build System Requirements for Python Projects
- PEP 621 – Storing project metadata in pyproject.toml
- PEP 735 – Dependency Groups in pyproject.toml
- Dependency Groups - Python Packaging User Guide
- Ruff - The Ruff Linter
- pytest-asyncio documentation
今回解説するファイル
[project]
name = "research-agent"
version = "0.1.0"
description = "description of research agent"
requires-python = ">=3.13"
dependencies = [
"langgraph>=0.3,<0.4",
"langchain-google-genai>=2.0,<3.0",
"tavily-python>=0.5,<0.6",
"boto3>=1.38,<2.0",
"pydantic>=2.10,<3.0",
]
[dependency-groups]
dev = [
"pytest>=8.3,<9.0",
"pytest-asyncio>=1.0,<2.0",
"respx>=0.21,<0.22",
"moto[dynamodb]>=5.0,<6.0",
"ruff>=0.9,<1.0",
"mypy>=1.14,<2.0",
"boto3-stubs[dynamodb,lambda]>=1.38,<2.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
[tool.ruff]
target-version = "py313"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
ignore = []
[tool.mypy]
python_version = "3.13"
strict = true
ignore_missing_imports = true
そもそも、なぜ TOML なのか?(JSON じゃないの?)
TypeScript エンジニアとして最初に感じた疑問が「なぜ JSON じゃないの?」でした。
Node.js では package.json が JSON 形式ですが、Python は公式パッケージング仕様(PEP 518)で TOML を採用しています。PEP 518 の中で、この選定理由は明確に述べられています。
1. コメントが書ける
JSON の仕様はコメント(// や #)をサポートしていません。package.json でも「なぜこのバージョンに固定したのか」をファイル内にメモできず、もどかしい思いをしたことがあるのではないでしょうか。TOML では # でコメントが書けるため、設定の意図を残すことができます。
# Pydantic V1 は非互換なため V2 系に固定
"pydantic>=2.10,<3.0",
2. 人間にとっての読み書きのしやすさ
PEP 518 の原文では、TOML が選ばれた理由として「human-usable(人間にとって使いやすい)であること(JSON とは異なり)」と明記されています。JSON は本来機械間のデータ通信を主目的としたフォーマットであり、ダブルクォーテーションや括弧の制約が厳しく、ネストが深くなりがちです。TOML は INI ファイルのような直感的な見た目を持ちつつ、厳格なデータ型も扱えます。
参照: PEP 518 – Specifying Minimum Build System Requirements for Python Projects
3. setup.py からの脱却
過去の Python では、プロジェクト管理に setup.py という Python スクリプトを実行していました。しかし、設定を読み込むために任意のコードを実行しなければならないことは、セキュリティリスクやツールによる静的解析を困難にする原因でした。これを解決するために、完全に「静的(コード実行不要)」でありながら表現力の高い TOML が公式仕様として採択されました。
セクションごとの解説
1. [project] — プロジェクトの基本情報と依存関係
[project]
name = "research-agent"
version = "0.1.0"
description = "description of research agent"
requires-python = ">=3.13"
dependencies = [
"langgraph>=0.3,<0.4",
"langchain-google-genai>=2.0,<3.0",
"tavily-python>=0.5,<0.6",
"boto3>=1.38,<2.0",
"pydantic>=2.10,<3.0",
]
この [project] テーブルは PEP 621 で標準化されたもので、プロジェクトのメタデータと本番依存関係を宣言する場所です。
package.json で例えると:
{
"name": "research-agent",
"version": "0.1.0",
"description": "description of research agent",
"engines": { "node": ">=22.0.0" }, // ≒ requires-python
"dependencies": {
"langgraph": ">=0.3.0 <0.4.0",
"langchain-google-genai": ">=2.0.0 <3.0.0",
"tavily-python": ">=0.5.0 <0.6.0",
"boto3": ">=1.38.0 <2.0.0",
"pydantic": ">=2.10.0 <3.0.0",
},
}
name, version, description はそのまま対応します。requires-python は package.json の engines.node に近い概念で、プロジェクトが動作する Python のバージョンを宣言します。dependencies は package.json の dependencies とほぼ同じ役割で、本番環境で必要なライブラリを列挙します。
バージョン指定の書き方は PEP 440 に従っており、>=0.3,<0.4 のように下限と上限を明示するスタイルが一般的です。Node.js の ^0.3.0(キャレット)に近い意図ですが、Python ではより明示的に範囲を書く文化があります。
2. [dependency-groups] — 開発環境専用の依存関係
[dependency-groups]
dev = [
"pytest>=8.3,<9.0",
"pytest-asyncio>=1.0,<2.0",
"respx>=0.21,<0.22",
"moto[dynamodb]>=5.0,<6.0",
"ruff>=0.9,<1.0",
"mypy>=1.14,<2.0",
"boto3-stubs[dynamodb,lambda]>=1.38,<2.0",
]
[dependency-groups] は PEP 735 として 2024 年 10 月に正式に承認(Accepted)された仕様です。ビルド成果物(配布パッケージ)には含まれない、開発時専用の依存関係を定義します。
package.json で例えると:
{
"devDependencies": {
"jest": ">=8.3.0 <9.0.0",
"eslint": ">=0.9.0 <1.0.0",
"typescript": ">=1.14.0 <2.0.0",
},
}
package.json の devDependencies に対応する概念です。npm install や yarn install で devDependencies が入るように、uv sync を実行すると dependency-groups の dev グループも自動的にインストールされます(uv はデフォルトで dev グループを同期します)。
dependency-groups が登場する以前は、[project.optional-dependencies] を使って開発依存を定義する方法がよく使われていましたが、optional-dependencies は本来「追加機能(extras)」のための仕組みであり、開発依存とは意味が異なります。PEP 735 はこの問題を解決するために設計されました。
なお、moto[dynamodb] や boto3-stubs[dynamodb,lambda] の [...] 部分は「extras」と呼ばれ、パッケージのオプション機能を選択的にインストールする仕組みです。Node.js にはこの概念に直接対応するものはありませんが、強いて言えば npm install package --with-feature のような選択的インストールに近いイメージです。
参照: PEP 735 – Dependency Groups in pyproject.toml / Dependency Groups - Python Packaging User Guide
3. [build-system] — ビルドシステムの設定
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src"]
[build-system] は PEP 518 で導入された最初のテーブルで、プロジェクトをビルドするために必要なツールとバックエンドを宣言します。
package.json で例えると:
{
"scripts": {
"build": "tsc", // TypeScriptをJSにコンパイル
},
"devDependencies": {
"typescript": "^5.0.0", // ビルドに必要なツール
},
}
requires はビルドを実行するために必要なパッケージ(ここでは hatchling)を指定します。build-backend はビルド処理のエントリーポイントを指定するもので、PEP 517 で標準化されたインターフェースです。
Python のビルドバックエンドには hatchling, setuptools, flit-core, pdm-backend などがあり、package.json で言えばバンドラー(tsc, esbuild, webpack など)を選ぶのに近い感覚です。
[tool.hatch.build.targets.wheel] の packages = ["src"] は、パッケージングの対象ディレクトリを src に限定する設定です。src レイアウト(ソースコードを src/ 配下に置く構成)は Python のモダンなプロジェクト構成として推奨されています。
4. [tool.*] — 各種開発ツールの設定
[tool] テーブルは、PEP 518 で「ツールが独自の設定を保存するための名前空間」として定義されました。package.json ではツールごとに別の設定ファイル(.eslintrc, tsconfig.json, jest.config.ts など)を作ることが多いですが、Python では pyproject.toml の [tool.*] に集約できるのが特徴です。
package.json で例えると:
// package.json 側にはツール設定はほとんど書かず、
// 別ファイルに分散することが多い:
// - .eslintrc.js → [tool.ruff] に相当
// - tsconfig.json → [tool.mypy] に相当
// - jest.config.ts → [tool.pytest.ini_options] に相当
4-1. [tool.pytest.ini_options] — テストランナーの設定
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
asyncio_mode = "auto" は、pytest-asyncio プラグインの設定です。auto モードでは、すべての async def test_... 関数に自動で @pytest.mark.asyncio マーカーが付与されます。つまり、各テスト関数にデコレータを手動で付ける必要がなくなります。
testpaths = ["tests"] は、テストコードの探索先を tests ディレクトリに限定する設定です。jest.config.ts の roots: ["<rootDir>/tests"] に相当します。
4-2. [tool.ruff] / [tool.ruff.lint] — リンター / フォーマッターの設定
[tool.ruff]
target-version = "py313"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
ignore = []
Ruff は Rust で書かれた超高速の Python リンター兼フォーマッターです。Flake8, isort, pyupgrade, Black など複数のツールを一つに統合した存在で、ESLint + Prettier を一つのツールにまとめたようなイメージです。
target-version = "py313" は、Python 3.13 向けにルールを最適化する設定です。tsconfig.json の "target": "ES2024" に近い概念です。
select で有効にしているルール群の意味は以下の通りです:
| コード | 由来 | 説明 |
|---|---|---|
E |
pycodestyle | PEP 8 スタイル違反(インデント、空白など) |
F |
Pyflakes | 論理エラーの検出(未使用 import、未定義変数など) |
I |
isort | import 文のソート順 |
UP |
pyupgrade | 古い Python 構文の検出と最新構文への書き換え |
B |
flake8-bugbear | バグになりやすいパターンの検出 |
SIM |
flake8-simplify | より簡潔に書けるコードの提案 |
この組み合わせは Ruff 公式ドキュメントでも推奨されている構成です。
4-3. [tool.mypy] — 静的型チェッカーの設定
[tool.mypy]
python_version = "3.13"
strict = true
ignore_missing_imports = true
mypy は Python の静的型チェッカーで、TypeScript における tsc --noEmit(型チェックのみ)に相当します。
strict = true は、最も厳格な型チェックを有効にする設定です。tsconfig.json の "strict": true とまったく同じ発想で、暗黙の Any 型を許可しない、戻り値の型アノテーションを強制するなど、複数のフラグをまとめて有効にします。
ignore_missing_imports = true は、型スタブ(型定義ファイル)が存在しないサードパーティライブラリに対するエラーを抑制する設定です。TypeScript で言えば @types/xxx が存在しないパッケージに対して // @ts-ignore するのと似た対処です。このプロジェクトでは boto3-stubs で AWS SDK の型定義を補完していますが、すべてのライブラリに型定義が用意されているわけではないため、この設定でカバーしています。
まとめ
| pyproject.toml のセクション | package.json での対応 |
|---|---|
[project] の name, version, description
|
name, version, description
|
requires-python |
engines.node |
dependencies |
dependencies |
[dependency-groups] の dev
|
devDependencies |
[build-system] |
scripts.build + ビルドツールの devDependencies
|
[tool.ruff] |
.eslintrc + .prettierrc
|
[tool.mypy] |
tsconfig.json(型チェック部分) |
[tool.pytest.ini_options] |
jest.config.ts |
TypeScript エンジニアの目から見ると、pyproject.toml は「package.json + tsconfig.json + .eslintrc + jest.config.ts を一つのファイルにまとめたもの」と理解すると、全体像が掴みやすくなります。
Python のパッケージング仕様は PEP 518(2016 年)→ PEP 621(2020 年)→ PEP 735(2024 年)と段階的に整備されてきました。uv のようなモダンなパッケージマネージャーを使えば、これらの仕様をフルに活用した快適な開発体験を得ることができます。