プロジェクト管理したい(pyproject.toml)

 1[project]
 2name = "PyPIに公開するプロジェクト名"
 3version = "..."
 4description = "..."
 5readme = "README.md"
 6requires-python = ">=3.10"
 7authors = [
 8    {name = "qumasan", email = "..."}
 9]
10license = { text = "MIT" }
11keywords = ["...", "..."]
12dependencies = [
13    "pydantic",
14    "typer",
15    "rich",
16    "loguru",
17]
18
19[project.optional-dependencies]
20docs = [
21    "zensical",
22]
23
24dev = [
25    "pytest",
26    "pytest-cov",
27    "ruff",
28    "commitizen",
29    "pre-commit",
30]
31
32[project.scripts]
33script_name = "..."
34
35[project.urls]
36Repository = "..."
37Documentation = "..."
38Issues = "..."
39
40[build-system]
41requires = ["hatchling"]
42build-backend = "hatchling.build"
43
44[tool.uv]
45managed = true
46
47[tool.ruff]
48line-length = 100
49target-version = "py312"
50
51[tool.commitizen]
52name = "cz_conventional_commits"
53tag_format = "$version"
54version_scheme = "semver2"
55version_provider = "uv"
56update_changelog_on_bump = true
57major_version_zero = true
58version_files = [
59    "..."
60]

pyproject.tomlは、Pythonプロジェクトの設定を一元管理するための標準ファイルです。 パッケージ情報、依存関係、ツール設定、ビルド設定などを一箇所にまとめて書くことができます。

注釈

長い間、Pythonのプロジェクト設定は、setup.pyやsetup.cfg、requirements.txtなどに分散して、正しい書き方を探すのが大変でした。

2016年のPEP 518でpyproject.tomlが導入され、 2018年に登場したpoetryが採用したことで、デファクトスタンダードとして使われるようになりました。

その後、 PEP 517(ビルドバックエンドの標準化)、 PEP 621([project]メタデータの標準化) が策定され、uvやhatchなどモダンなツールの登場により標準化が進みました。

注釈

2016年の段階でTOML形式の設定ファイルが導入されたのは不思議だったなぁと思っています。 当時のPythonの標準ライブラリにはTOMLパーサーが含まれておらず、tomliやtomllibなどの外部ライブラリが必要でした。 またTOML形式の仕様も固まってない時期です。

そのため、自作ツールの設定は、まだまだINI形式やYAML形式で書く方が簡単で、結局、設定ファイルが2つに増えた印象でした。 2022年のPython 3.11で、ようやく標準ライブラリにTOMLパーサーが含まれるようになりました。

プロジェクト設定したい([project])

 1[project]
 2name = "PyPIに公開するプロジェクト名"
 3version = "..."
 4description = "..."
 5readme = "README.md"
 6requires-python = ">=3.10"
 7authors = [
 8    {name = "qumasan", email = "..."}
 9]
10license = { text = "MIT" }
11keywords = ["...", "..."]

[project]テーブルに、プロジェクトのメタデータを書きます。 PEP 621で必須とされているのはnameだけです。 versionも必須ですが、dynamic = ["version"]と書けば、 ビルドツール側に動的に決定させることもできます(commitizenやhatch versionなどと組み合わせる場合に便利です)。 それ以外のdescription、readme、authors、license、keywordsはすべて任意です。

カテゴリーしたい(classifiers)

1[project]
2classifiers = [
3    "Development Status :: 3 - Alpha",
4    "Intended Audience :: Science/Research",
5    "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
6    "Programming Language :: Python :: 3",
7    "Programming Language :: Python :: 3.12",
8]

[project]テーブルのclassifiersに、PyPIで規定されている定型のカテゴリー文字列(Trove classifiers)を書けます。 開発状況、対象ユーザー、ライセンス、対応Pythonバージョンなどを、PyPIのプロジェクトページで検索・絞り込みできるようにする分類です。 書式が自由なkeywordsと違い、 公式の一覧 から選んで書く必要があります。

依存パッケージしたい(dependencies)

1[project]
2dependencies = [
3    "pydantic",
4    "typer",
5    "rich",
6    "loguru",
7]

[project]テーブルのdependenciesキーに、プロジェクトの実行に必要な依存パッケージを書きます。 バージョン指定する場合は "pydantic>=2.0" のように、PEP 508形式の文字列で書きます。

オプション依存パッケージしたい([project.optional-dependencies])

 1[project.optional-dependencies]
 2docs = [
 3    "zensical",
 4]
 5
 6dev = [
 7    "pytest",
 8    "pytest-cov",
 9    "ruff",
10    "commitizen",
11    "pre-commit",
12]

インストール時に選択できる、追加の依存パッケージ群を定義します。 キーがextra名(docs、devなど)、値がその依存パッケージの配列です。 利用者はpip install my-project[docs]のように、extra名を指定してインストールできます。

devのように開発専用のツールをここに置くこともできますが、 配布先(PyPIなど)にextraとして公開される点に注意してください。 CI/CD専用など、パッケージの利用者に見せる必要のない依存は、 uvやpoetryが対応する[dependency-groups](PEP 735)で管理する方が適切です。

関連リンクしたい([project.urls])

1[project.urls]
2Repository = "https://gitlab.com/osechi/kazunoko"
3Documentation = "https://kazunoko.readthedocs.io"
4Issues = "https://gitlab.com/osechi/kazunoko/-/issues"

[project.urls]テーブルに、プロジェクト関連のリンクをまとめて書けます。 キーは自由に決められますが、 Repository(ソースコード)、 Documentation(ドキュメント)、 Homepage(公式サイト)、 Issues(課題管理) あたりがよく使われます。 PyPIのプロジェクトページに、サイドバーのリンクとして表示されます。

コマンド名したい([project.scripts])

1[project.scripts]
2cli-name = "module.path:app"  # src/module/path.pyのapp関数

[project.scripts]で、コマンド名を設定できます。 左辺のcli-nameがコマンド名、 右辺が実行する関数です。 モジュールまでのパスは.で区切り、 モジュール内の関数は:で区切ります。 module.path:appは、src/module/path.pyのapp()関数を指します。

ビルド環境したい([build-system])

[build-system]で、パッケージをビルドするためのツール(ビルドバックエンド)を指定できます。 requiresに必要なパッケージ、build-backendにビルド処理を担うモジュールを書きます(PEP 517、PEP 518)。 使っているプロジェクト管理ツールに合わせて選びます。

hatchしたい(hatchling)

1[build-system]
2requires = ["hatchling"]
3build-backend = "hatchling.build"

hatchのビルドバックエンドです。 hatch newで作成したプロジェクトでは、デフォルトでこの設定になります。

poetryしたい(poetry-core)

1[build-system]
2requires = ["poetry-core>=2.0.0,<3.0.0"]
3build-backend = "poetry.core.masonry.api"

poetryのビルドバックエンドです。 poetry newやpoetry initで作成したプロジェクトでは、デフォルトでこの設定になります。

uvしたい(uv_build)

1[build-system]
2requires = ["uv_build>=0.12.5,<0.13.0"]
3build-backend = "uv_build"

uvのビルドバックエンドです。 uv initで作成したプロジェクトでは、デフォルトでこの設定になります。 requiresのバージョン範囲は、インストールされているuvのバージョンに合わせて自動で設定されます。

注釈

uvはプロジェクト管理ツールであり、ビルドバックエンドとは独立した仕組みです。 [build-system]をhatchlingなど他のバックエンドに書き換えても、 uv buildやuv syncは問題なく動作します。

flitしたい(flit_core)

1[build-system]
2requires = ["flit_core>=3.11,<5"]
3build-backend = "flit_core.buildapi"

シンプルな単一パッケージの配布に特化したflitのビルドバックエンドです。 複雑な設定を必要としない、小規模なプロジェクトに向いています。

setuptoolsしたい(setuptools)

1[build-system]
2requires = ["setuptools"]
3build-backend = "setuptools.build_meta"

古くから使われているsetuptoolsのビルドバックエンドです。 [build-system]テーブル自体を省略した場合も、フォールバックとしてsetuptoolsが使われます。

それぞれの特徴

バックエンド

特徴

C拡張

hatchling

プラグインが豊富で設定の柔軟性が高い

プラグイン経由

poetry-core

poetryと一体で、依存関係管理に強い

未対応

uv_build

最速・ゼロコンフィグ志向

非対応

flit_core

最小限・軽量、設定項目が少ない

非対応

setuptools

もっとも枯れていて実績が豊富

標準対応

生成されるwheel・sdistはどのバックエンドでも標準形式(PEP 517・PEP 427)なので、 pip installする利用者側は違いを意識する必要はありません。 一方、[tool.poetry.*]や[tool.hatch.*]のようなツール固有の設定は共通化されていないため、 バックエンドを乗り換える際は書き直しが必要になることがあります。

C拡張やCythonなど、コンパイルが必要なパッケージを作る場合はsetuptoolsを選ぶのが無難です。 それ以外の純粋なPythonパッケージであれば、使っているプロジェクト管理ツール純正のバックエンドを選ぶと摩擦が少ないです。

ツール設定したい(tool.ツール名)

1[tool.uv]
2managed = true

[tool.ツール名]テーブルに、各ツール独自の設定を書きます。 [project]や[build-system]と違って標準化されておらず、書式や項目はツールごとに異なります。 [tool.uv]はuvの設定です。 managed = true(デフォルト)で、このプロジェクトをuvが管理していることを明示します。

1[tool.ruff]
2line-length = 100
3target-version = "py312"

[tool.ruff]はRuffの設定です。 line-lengthで1行の最大文字数、target-versionで対象のPythonバージョンを指定できます。

1[tool.commitizen]
2name = "cz_conventional_commits"
3version_provider = "pep621"
4tag_format = "v$version"

[tool.commitizen]はcommitizenの設定です。 version_providerで、バージョン番号をどこから読み書きするか指定します。 "pep621"を指定すると、[project]のversionキーと連動します。

注釈

[tool.hatch.build.targets.wheel]のように、ツール名の下にさらに階層を作ることもできます。 どんなキーが使えるかは、それぞれのツールの公式ドキュメントを参照してください。

パッケージ名とコマンド名を別々にしたい

 1[project]
 2name = "osechi-kazunoko"
 3
 4[project.scripts]
 5kazunoko = "kazunoko.cli:app"  # src/kazunoko/cli.pyのapp関数
 6
 7[build-system]
 8requires = ["hatchling"]
 9build-backend = "hatchling.build"
10
11[tool.hatch.build.targets.wheel]
12packages = ["src/kazunoko"]

パッケージ名とコマンド名を別々に設定することで、 PyPIで配布するパッケージ名の衝突を避けつつ、 ユーザーが実行するコマンド名を短く設定できます。

しかし、実際に設定してみようとしたら、少し躓いたので整理しました。

上記は、 パッケージ名をosechi-kazunokoとして、 コマンド名をkazunokoとして設定した例です。

[project]のnameにパッケージ名を、 [project.scripts]のキーにコマンド名を設定します。

躓いたのは[build-system]の設定でした。 uv_buildではうまくいかず、hatchlingを指定し、 さらに [tool.hatch.build.targets.wheel]のpackagesに、パッケージのソースが入っているディレクトリ名 (この例ではsrc/kazunoko)の設定が必要でした。

コマンドを一時的に実行する場合は uvx --from osechi-kazunoko kazunoko と打てばOKです。