Claude CodeでPython CLIのパッケージ化とuv tool installでの配布
argparseのスクリプトをpyproject.tomlでパッケージ化し、uv tool installで社内に配る手順です。entry pointの定義、venvでの動作確認、Claude Codeに任せる完了条件までまとめます。
Claude CodeでPython CLIのパッケージ化とuv tool installでの配布
argparseで書いたスクリプトは、pyproject.tomlに[project.scripts]を1つ書くとコマンドとして配れる形になります。受け取る側はuv tool installを1回実行するだけで、Pythonの依存を意識せずに使えます。Claude Codeに任せるときは、「インストールして--helpが通り、終了コードが期待どおり」までを完了条件に置くと、手戻りが減ります。
この記事では、最小のパッケージを実際にvenvへインストールして確かめた出力を示し、その後にuv tool installでの配布と、Claude Code側の指示の書き方を扱います。
何を決めればパッケージになるのか
Pythonのパッケージ化に必要なのは、pyproject.tomlの3つの表です。[project]で名前と版を、[build-system]でビルド役を、[project.scripts]でコマンド名を決めます。
スクリプトがコマンドになるまで
- 1
関数に切り出す
処理を
main()のような関数にし、if __name__ == "__main__":の中身を1行にします。 - 2
src配下に置く
src/mytool/cli.pyのように、パッケージ名のディレクトリに入れます。 - 3
pyproject.tomlに3つの表を書く
[project]と[build-system]と[project.scripts]です。 - 4
インストールして叩く
--helpと、わざと引数を欠いた実行の2つで、挙動を確かめます。
最小のpyproject.tomlは次のとおりです。ビルド役にはHatchlingを選んでいます。packaging.python.orgのガイドが並べる代表的なバックエンドの1つで、requiresとbuild-backendの2行をそのまま写せます。
[project]
name = "mytool"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = []
[project.scripts]
mytool = "mytool.cli:main"
[build-system]
requires = ["hatchling >= 1.26"]
build-backend = "hatchling.build"dependenciesには、スクリプトがimportする外部パッケージを書きます。argparseは標準ライブラリなので、この例では空です。外部ライブラリを使うなら"httpx"や"rich>=14"のように並べます。
entry pointは「モジュール:関数」の1行で決まる
[project.scripts]の右辺はパッケージ.モジュール:関数名の形です。上の例のmytool = "mytool.cli:main"は、mytoolというコマンドを作り、実行されたらmytool.cliモジュールのmainを呼ぶ、という意味になります。
ガイドの説明では、インストール後にこのコマンドを実行するとimport sys; from spam import main_cli; sys.exit(main_cli())と同じ処理になります。ここから実装側の約束が決まります。
main()の戻り値が、そのままプロセスの終了コードになる- 戻り値が
Noneなら終了コードは0になる - 失敗を伝えたいときは、
return 1かsys.exit(1)を使う
実装は次のような形です。main(argv=None)と引数を受けておくと、後でテストからmain(["bob"])のように直接呼べます。
import argparse
import sys
def main(argv=None):
p = argparse.ArgumentParser(prog="mytool")
p.add_argument("name")
p.add_argument("--shout", action="store_true")
a = p.parse_args(argv)
msg = f"hello {a.name}"
print(msg.upper() if a.shout else msg)
return 0
if __name__ == "__main__":
sys.exit(main())Windowsでは[project.scripts]で作ったコマンドに端末が必要です。GUIアプリから起動して端末を出したくないコマンドは、[project.gui-scripts]に書きます。両者の違いはWindowsでだけ意味を持ちます。
venvに入れて、出力で完成を判定する
配る前に、空のvenvへインストールして動かします。ここでは手元のPython 3.12.2で、pipを使って確かめました。uvでも同じpyproject.tomlが使えるので、ビルド結果の確認としてはこれで足ります。
python3 -m venv v && ./v/bin/pip install -q .
./v/bin/mytool --help
./v/bin/mytool bob --shout; echo rc=$?
./v/bin/mytool; echo rc=$?出力は次のようになりました。
usage: mytool [-h] [--shout] name
positional arguments:
name
options:
-h, --help show this help message and exit
--shout
HELLO BOB
rc=0
usage: mytool [-h] [--shout] name
mytool: error: the following arguments are required: name
rc=2見るべき点は3つです。--helpにprog="mytool"の名前が出ること、正常系の終了コードが0であること、引数を欠いたときにargparseが終了コード2で止まることです。引数エラーの2は、この3点の最後の確認材料になります。
生成された起動スクリプトの中身も見られます。v/bin/mytoolを開くと、先頭にfrom mytool.cli import mainがあり、末尾でsys.exit(main())を呼ぶだけの短いファイルでした。ガイドの「import sys; from spam import main_cli; sys.exit(main_cli())と同等」という説明と一致します。コマンドが動かないときは、このファイルのimport先が実在するかを最初に疑うと切り分けが早くなります。
コマンドが見つからない・動かないときの切り分け
- Qvenvの bin 配下に mytool ができているか?答えがいいえのとき答えがはい
[project.scripts]の表が[project]の下に正しく書かれているか、インストールが成功したかを見ます。 - Q起動スクリプトのimport先は実在するか?答えがいいえのとき答えがはい
mytool.cli:mainのモジュール名と関数名の綴りを直します。src配下のディレクトリ名とも突き合わせます。
entry pointは正しい
起動スクリプトに問題はありません。引数処理か実装の側を疑います。
uv tool installで配る
venvで動いたら、配布はuv tool installに任せます。uv tool installは、パッケージを専用の隔離環境に入れ、実行ファイルをPATH上のディレクトリにリンクします。受け取る側は、Pythonの環境を作らずにコマンドだけ使えます。
# 開発中: ソースの変更がそのまま反映される
uv tool install --editable .
# 社内のGitリポジトリから配る
uv tool install git+https://git.example.com/team/mytool
# 入ったか確認し、PATHを通す
uv tool list
uv tool update-shell
mytool --helpgit+から入れる形はuvのガイドにある書き方で、配布先のリポジトリURLを渡すだけで済みます。PyPIに公開しない社内ツールでも、リポジトリにpyproject.tomlがあれば成立します。
ガイドには、実行ファイルがPATHに無いときは警告が出て、uv tool update-shellでシェル設定に追記できる、とあります。受け取る側への案内には、この1行を添えておくと問い合わせが減ります。
更新と撤去のコマンドは次のとおりです。
| 目的 | コマンド |
|---|---|
| 最新版へ更新 | コマンドuv tool upgrade mytool |
| すべて更新 | コマンドuv tool upgrade --all |
| 入れ直し | コマンドuv tool install --force mytool |
| 一覧 | コマンドuv tool list |
| 撤去 | コマンドuv tool uninstall mytool |
uv tool upgradeは、インストール時に付けたバージョン制約を守ります。たとえば>=0.3,<0.4で入れた場合、その範囲の最新版までしか上がりません。制約そのものを変えたいときは、uv tool installで入れ直します。
配るときに踏みやすい落とし穴
落とし穴
入れた後に python -c 'import mytool' が失敗する
uv tool installで入れたパッケージは隔離環境にあり、手元のPython環境からはimportできません。uvのガイドも、ruffを入れてもpython -c "import ruff"は失敗する例を挙げています。ライブラリとしても使わせたいときは、別にライブラリ用の配布方法を用意します。
ほかに、設計時に決めておくと迷わない点が3つあります。
- コマンド名の衝突: uvは、自分が入れていない実行ファイルを上書きしません。たとえばpipxで入れた同名のツールがあると、
uv tool installは失敗します。--forceで上書きできますが、既存のツールを消す操作になります。 - Python本体の撤去: ツールが使っているPythonをアンインストールすると、そのツールの環境が壊れます。
uv tool install --python 3.12 mytoolのように版を指定し、requires-pythonと揃えておくと運用が読みやすくなります。 - 依存のコマンド: 依存パッケージが持つコマンドは、PATHにリンクされません。複数のコマンドをまとめて配りたいときは、
--with-executables-fromを使います。
versionを手で書き換え忘れると、uv tool upgradeしても更新が反映されたか分かりません。ビルド役が版を計算するdynamic = ["version"]にしておく手もあり、その場合は計算の仕方をビルド役のドキュメントで確かめます。
依存と対応Pythonの書き方
外部ライブラリを使うCLIでは、dependenciesとrequires-pythonが、受け取る側で動くかどうかを決めます。
[project]
requires-python = ">=3.9"
dependencies = [
"httpx",
"rich>=14.2",
]
[project.optional-dependencies]
dev = ["pytest"]requires-pythonは、対応する最小のPythonです。社内の端末が3.10までしかないのに>=3.12と書くと、インストール時に弾かれます。配布先の最も古い環境に合わせて決めます。
dependenciesの版指定は、緩すぎても厳しすぎても運用が痛みます。"httpx"のように無指定だと、将来の破壊的変更を拾います。==で固定すると、受け取る側の他ツールと衝突しやすくなります。下限だけ>=で置くのが、社内CLIでは落ち着きやすい書き方です。
テスト用のpytestのように、実行には要らない依存は[project.optional-dependencies]へ分けます。pip install .[dev]のように、extraを指定したときだけ入ります。Claude Codeに「テストを回して」と頼む場面では、このextraを入れた開発環境を使わせます。
インストールせずに試すか、入れてしまうか
uvには、入れずに一度だけ実行するuvxもあります。uvxは一時的な隔離環境でツールを走らせ、uv tool installは永続する環境に入れてPATHに通します。
uvのガイドは、多くの場合はuvxで実行するほうが向いていて、インストールが要るのは他のプログラムからそのコマンドを呼びたいときなどだと説明しています。社内CLIでは、次のように分けると案内しやすくなります。
| 使い方 | 向く方法 |
|---|---|
| 月に数回、ときどき使う | 向く方法uvx |
| 毎日使い、シェルから直接呼ぶ | 向く方法uv tool install |
| 別のスクリプトやフックから呼ぶ | 向く方法uv tool install |
Claude CodeのHookやSkillからコマンドを呼ばせる場合は、毎回環境を作らないuv tool installのほうが、起動が安定します。
Claude Codeに任せるときの書き方
ここまでの手順を、Claude Codeが毎回なぞれるよう、完了条件を文章にして渡します。プロジェクトのCLAUDE.mdには次のように書けます。
## CLIのパッケージ化
- コマンドは `pyproject.toml` の `[project.scripts]` で定義する。直接 `python x.py` を配布手順にしない
- 変更後は、空のvenvに `pip install .` してから確かめる
1. `<コマンド> --help` が終了コード0で、prog名が出る
2. 必須引数を欠いた実行が終了コード2になる
3. 正常系の出力と終了コードがテストと一致する
- 3つがそろうまで完了と報告しない
- `uv tool install` は利用者側の操作なので、この環境では実行しない最後の1行が要点です。uv tool installはPATH上のディレクトリにリンクを作るため、開発中のマシンで勝手に走らせると、環境を変えてしまいます。確認はvenvに閉じ、配布の操作は人に残す形にしておきます。
同じ確認を繰り返し頼むなら、Skillにすると呼び出しが1語で済みます。.claude/skills/verify-cli/SKILL.mdは次のとおりです。
---
name: verify-cli
description: パッケージ化したCLIを空のvenvに入れて、--helpと終了コードを確かめる。CLIの配布前に使う。
allowed-tools: Bash(python3 -m venv *) Bash(./v/bin/*)
---allowed-toolsは、そのSkillを呼んだターンの間だけ、列挙したBashコマンドを確認なしで走らせます。次のメッセージを送ると付与は消えます。ほかのツールが使えなくなるわけではなく、列挙外の権限は通常の設定に従います。リポジトリに入れたSkillはallowed-toolsごと配られるので、パターンを広げすぎないことが大事です。ここでは、venvの作成と、そのvenv内の実行ファイルだけに絞っています。
テストの書き方はClaude CodeでTyper製CLIを作るにまとめています。そこで扱うCliRunnerのテストを足すと、上の3条件のうち3つ目を機械で判定できます。uvプロジェクトの権限設定はuv runとpermissionsの設定が詳しいです。
uv tool installとexe化の使い分け
Pythonが入っていない端末へ配るなら、exe化が選択肢に入ります。手順はPyInstallerのexe化を任せる手順で扱っています。
| 配布先の状況 | 向く方法 | 理由 |
|---|---|---|
| 開発者が多く、uvを入れられる | 向く方法uv tool install | 理由更新がuv tool upgradeで済む |
| Pythonもuvも無い端末が混ざる | 向く方法exe化 | 理由受け取る側の前提が要らない |
| OSごとに挙動を揃えたい | 向く方法exe化 | 理由同梱するPythonの版を固定できる |
まとめ
CLIの配布は、[project.scripts]の1行と、venvで確かめる3条件で形になります。配る方法は、受け取る側にuvを入れられるかで決まります。入れられるならuv tool installが最短で、入れられないならexe化へ進みます。Claude Codeには、手順そのものより「何が出たら完了か」を渡すと、確認の抜けが起きにくくなります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。