Python ファイル1つで依存ライブラリ管理もできるポータブル実行スクリプトを作る

Python で「ちょっとしたスクリプト」を書きたい時、外部ライブラリを使おうとすると途端に面倒になる。pyproject.toml を作って uv add (もしくは uv sync) で依存をインストールして、できれば仮想環境も切って…という手順が必要だった。
これを Python ファイル1個だけで完結させる方法がある。PEP 723 (Inline script metadata, 2024 採択) と uv の組み合わせを使う。
完成形
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["jinja2>=3.1", "requests>=2.31"]
# ///
"""Render a Jinja2 template fetched from a URL."""
import sys
import requests
from jinja2 import Template
resp = requests.get(sys.argv[1])
print(Template(resp.text).render(name="world"))
これを保存して chmod +x script.py するだけで、
./script.py https://example.com/template.j2
で実行できる。pip install も python -m venv も不要。Python 自体が入っていなくても uv が自動でダウンロードする。
各行の解説
shebang 行
#!/usr/bin/env -S uv run --script
#!= Unix の shebang。カーネルがファイル先頭を見て「これで実行しろ」と解釈する/usr/bin/env -S=envコマンドの-Sオプション。これは「shebang の引数を空白で複数に分割していい」という意味。これを付けないとuv run --scriptが 1つの実行ファイル名として解釈されてcommand not foundになる- macOS と Linux 4.0+ ならサポートされている
- Ubuntu 18.04 以前など古い環境では動かない
uv run --script= uv に「このファイルを単体スクリプトモードで実行しろ」と指示
PEP 723 メタデータブロック
# /// script
# requires-python = ">=3.11"
# dependencies = ["jinja2>=3.1", "requests>=2.31"]
# ///
これが PEP 723 で標準化された inline script metadata の構文。
# /// script〜# ///で囲まれたブロック内の行は、各行の#プレフィックスを剥がしてから TOML としてパースされる- 普通のコメントとして書かれているので、PEP 723 非対応のツール (古い IDE, Linter 等) からはただのコメントとして無視される。互換性が壊れない
- フィールドは
pyproject.tomlの[project]テーブルと同じスキーマrequires-python= 必要な Python バージョン (PEP 440 形式)dependencies= 依存パッケージ (PEP 508 形式)
実行時に何が起きるか
./script.py を実行した時の流れ:
- カーネルが shebang を読んで
/usr/bin/env -S uv run --script /path/to/script.pyを起動する - uv がファイル冒頭の
# /// scriptブロックを TOML としてパースする - uv が
~/.cache/uv/に このスクリプト専用の隔離仮想環境を作る (初回のみ、スクリプトのハッシュベースでキャッシュ) dependenciesで指定したパッケージをその環境にインストールする (初回のみ)requires-pythonを満たす Python を探す。なければ uv が自動でダウンロードする- その隔離環境で
script.pyを実行する
従来との比較
| 項目 | 従来 (uv プロジェクト方式) | inline metadata + uv shebang |
|---|---|---|
| 依存管理 | pyproject.toml を別途作成 |
スクリプト1ファイルで完結 |
| 仮想環境 | uv venv などで作成 |
uv が自動で作成・キャッシュ |
| インストール | uv add jinja2 / uv sync |
初回実行時に自動 |
| Python バージョン管理 | .python-version などで指定 |
uv が自動取得 |
| 実行 | uv run python script.py |
./script.py 直接 |
| 配布 | プロジェクトディレクトリごと配る | 1ファイルをメール添付・Gist でOK |
数値で見るメリット
- 初回実行: 〜3秒 (依存DLと Python 取得込み)
- 2回目以降: 〜200ms (キャッシュヒット)
- 配布性: スクリプト1ファイルで完結。Gist 1個・メール添付・Slack 貼り付けで動く
- 環境汚染ゼロ: グローバル Python に何もインストールしない、隔離環境で動く
落とし穴
1. env -S は古い Linux で動かない
-S フラグは coreutils 8.30 (2018) からの機能。Ubuntu 18.04 以前、CentOS 7 などでは使えない。
回避策: shebang を書かずに、明示的に uv run --script script.py で起動する。
2. uv のインストールが前提
curl -LsSf https://astral.sh/uv/install.sh | sh
uv 自体は単一バイナリで配布されており、インストールも軽量 (Rust 製)。
3. キャッシュディレクトリの権限
~/.cache/uv/ への書き込み権限がないと失敗する。CI 環境やコンテナ内、サンドボックス環境などで問題になることがある。
環境変数で変更できる:
UV_CACHE_DIR=/tmp/uv-cache ./script.py
4. IDE が依存を認識しない
VSCode の Pylance などは uv が解決した仮想環境を見つけられないため、Import "jinja2" could not be resolved のような警告が出ることがある。
対処:
- 警告を無視する (実害はない)
- ファイル冒頭に
# pyright: reportMissingImports=falseを追加する uv venv && source .venv/bin/activate && uv pip install jinja2で IDE 用の環境を別途作る
5. PEP 723 対応ツールはまだ少ない
uv 以外だと Hatch, pdm が対応中。pip 単体ではこの機能を解釈できない。
ユースケース
向いている場面:
- 配布したい一発ものスクリプト (社内ツール、Gist で共有するスニペット)
- CI でしか動かさない補助スクリプト
- ChatGPT / Claude に書いてもらった「あの処理」を1ファイルで実行したい時
- ブログ記事に貼るサンプルコード
- 依存ライブラリのある自作 CLI ツールを軽く配布したい時
向いていない場面:
- 複数ファイルに分割される本格アプリケーション (普通に
pyproject.tomlを使う) - 起動レイテンシをとことん削りたい用途 (それでもキャッシュ後は 200ms 程度なので大半の用途は許容範囲)
まとめ
Python は長らく「shebang 1行で動かない」「依存があると環境構築が面倒」と言われてきた。
PEP 723 + uv の組み合わせで、Bash や Node.js が当たり前にやってきた「ファイル1つで完結する実行可能スクリプト」が Python でも自然にできるようになった。
ちょっとした自動化スクリプトを書く時は、もうこれが標準でいい。
参考
開発相談をお待ちしています。