---
slug: "portable-python-script-with-pep723-and-uv"
title: "Python ファイル1つで依存ライブラリ管理もできるポータブル実行スクリプトを作る"
description: "PEP 723 (Inline script metadata) と uv の shebang サポートを組み合わせて、依存ライブラリ管理も含めて Python ファイル1つで完結する実行可能スクリプトを書く方法を解説する。"
url: "https://www.ytyng.com/blog/portable-python-script-with-pep723-and-uv"
publish_date: "2026-05-01T02:40:27.986Z"
created: "2026-05-01T02:40:27.987Z"
updated: "2026-05-01T06:23:42.889Z"
categories: []
keywords: ""
featured_image_url: "https://media.ytyng.com/resize/20260501/c503171d0fb6461b8b5283ba15983ce9.png.webp?width=768"
has_video: true
has_music: true
video_urls: ["https://media.ytyng.net/ytyng-blog/344/featured-video-1.mp4", "https://media.ytyng.net/ytyng-blog/344/featured-video-2.mp4", "https://media.ytyng.net/ytyng-blog/344/featured-video-3.mp4"]
music_urls: ["https://media.ytyng.net/ytyng-blog/344/featured-music-344-1.mp3", "https://media.ytyng.net/ytyng-blog/344/featured-music-344-2.mp3"]
lang: "ja"
---

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

Python で「ちょっとしたスクリプト」を書きたい時、外部ライブラリを使おうとすると途端に面倒になる。`pyproject.toml` を作って `uv add` (もしくは `uv sync`) で依存をインストールして、できれば仮想環境も切って…という手順が必要だった。

これを **Python ファイル1個だけで完結させる**方法がある。`PEP 723` (Inline script metadata, 2024 採択) と `uv` の組み合わせを使う。

## 完成形

```python
#!/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` するだけで、

```bash
./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` を実行した時の流れ:

1. カーネルが shebang を読んで `/usr/bin/env -S uv run --script /path/to/script.py` を起動する
2. uv がファイル冒頭の `# /// script` ブロックを TOML としてパースする
3. uv が `~/.cache/uv/` に **このスクリプト専用の隔離仮想環境**を作る (初回のみ、スクリプトのハッシュベースでキャッシュ)
4. `dependencies` で指定したパッケージをその環境にインストールする (初回のみ)
5. `requires-python` を満たす Python を探す。なければ uv が自動でダウンロードする
6. その隔離環境で `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 のインストールが前提

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

uv 自体は単一バイナリで配布されており、インストールも軽量 (Rust 製)。

### 3. キャッシュディレクトリの権限

`~/.cache/uv/` への書き込み権限がないと失敗する。CI 環境やコンテナ内、サンドボックス環境などで問題になることがある。

環境変数で変更できる:

```bash
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 でも自然にできる**ようになった。

ちょっとした自動化スクリプトを書く時は、もうこれが標準でいい。

## 参考

- [PEP 723 — Inline script metadata](https://peps.python.org/pep-0723/)
- [uv: Running scripts](https://docs.astral.sh/uv/guides/scripts/)
- [astral-sh/uv (GitHub)](https://github.com/astral-sh/uv)

