はじめに
Claude Code などのコーディングエージェントは、タスクを進める途中で、その場で Python コードを書いてインラインで実行することがあります。
gh
の JSON 出力を整形したり、SQL ファイルから件数を数えたりと、シェル芸だけでは面倒な処理を Python で埋めてくることが多いです。
自分の Claude Code の会話履歴(
~/.claude/projects/*.jsonl
)を全て集計してみたところ、意外にも、その場で書いて実行するコードは 10 プロジェクトを合わせても 20 件強でした。ただし、出てくる場所と使うパターンは決まっていて、標準ライブラリのごく一部で足りるものがほとんどです。
この記事では、集計から見えてきた頻出パターンを整理して、Claude が書いた Python コードを読むために押さえておきたい要素をまとめます。
Claude が Python を呼ぶ 3 つの形
Claude が Python を実行する方法は、大きく 3 つに分かれます。
-
python3 -c '...'… 1〜2 行のワンライナー。シェルパイプの一部として使う -
python3 << 'PY' ... PY… ヒアドキュメント。数十行の処理をシェル履歴に残す -
python3 foo.py…Writeツールで.pyファイルを書いてから実行
私の履歴では、
-c
が 13 件、ヒアドキュメントが 7 件、
.py
ファイルの作成は 2 件でした。1 回きりの処理なら
-c
かヒアドキュメント、繰り返し使うものだけを
.py
に書き出す使い分けになっています。
パターン 1: JSON をパイプで整形する
もっともよく出てくるパターンです。
gh api
や
curl
の JSON 出力から、必要なフィールドだけを抜き出すのに使います。
たとえば、GitHub の PR コメントを整形する処理はこのような形になります。
| 1 2 | gh api repos/foo/bar/pulls/123/comments \ | python3 -c "import sys, json; d = json.load(sys.stdin); print(json.dumps([{'user': c['user']['login'], 'path': c['path'], 'body': c['body']} for c in d], ensure_ascii=False, indent=2))" |
シェルの引用符の中に詰め込まれていて読みにくいので、Python コードの部分だけを取り出して整形すると、次の処理をしていることが分かります。
| 1 2 3 4 5 6 7 8 | import sys, json comments = json.load(sys.stdin) result = [ {'user': c['user']['login'], 'path': c['path'], 'body': c['body']} for c in comments ] print(json.dumps(result, ensure_ascii=False, indent=2)) |
見るべき要素は 3 つあります。
-
json.load(sys.stdin)… 標準入力の JSON をパースして Python のオブジェクトに変換します。ファイルから読むときはjson.load(open(path))、文字列から読むときはjson.loads(s)です。名前が似ていて紛らわしいのですが、末尾のsは string のsと覚えておくと迷いません。 - 内包表記
[{...} for c in comments]… リストを 1 行で組み立てる書き方です。詳しくは次の節で説明します。 -
json.dumps(..., ensure_ascii=False, indent=2)… Python オブジェクトを JSON 文字列に戻します。indent=2は改行付きで整形、ensure_ascii=Falseが日本語プロジェクトでは特に重要です。
loadと同様にこちらもjson.dump()関数を使ってファイル等に書き込むこともできます。
内包表記
内包表記は、
for
文で空のリストに要素を
append
していく処理を、1 行で書く書き方です。上のコードの
result = [...]
の部分は、次のループと同じ意味になります。
| 1 2 3 | result = [] for c in comments: result.append({'user': c['user']['login'], 'path': c['path'], 'body': c['body']}) |
後ろに
if
を付けると、条件で絞り込むこともできます。
| 1 2 3 4 5 6 | # body が空でないコメントだけを対象にする result = [ {'user': c['user']['login'], 'body': c['body']} for c in comments if c['body'] ] |
[...]
を
{...}
に変えて
key: value
の形で書くと、辞書内包表記になります。
| 1 2 | # ユーザー名をキー、コメント本文をバリューにした dict を作る by_user = {c['user']['login']: c['body'] for c in comments} |
{...}
の中に
key: value
ではなく単一の式を書くと、セット内包表記になり、重複を除いた集合が得られます。
| 1 2 | # コメントを付けたユーザー名の集合 users = {c['user']['login'] for c in comments} |
丸括弧
(...)
にするとジェネレータ式になります。要素を都度生成するので、全要素をメモリに載せずに
sum
や
any
に流すときに便利です。
| 1 2 | # body の長さの合計を、途中のリストを作らずに求める total = sum(len(c['body']) for c in comments) |
内包表記は Python で頻繁に出てくる書き方なので、
for
と
if
の並び順、括弧の種類ごとの意味を掴んでおくと、Claude が書くコードもすんなり読めるようになります。
ensure_ascii=False の意味
json.dumps
はデフォルトで、非 ASCII 文字を
\uXXXX
形式にエスケープします。たとえば
{"name": "田中"}
は
{"name": "田中"}
と出力されます。JSON 仕様上は正しいのですが、人間には読めません。
ensure_ascii=False
を付けると、日本語がそのまま出力されるようになります。Claude が書くコードでは、日本語を扱うプロジェクトのときにこれが必ず付いていました。
パターン 2: 行番号でファイルを書き換える
sed -i
でもできる処理ですが、複数行の書き換えを dict でまとめて記述したいときや、事前チェックを入れたいときには Python で書く方が確実です。
たとえば、Markdown の表の特定行の末尾に列を追記する処理です。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | python3 << 'PY' path = "docs/api.md" with open(path, encoding="utf-8") as f: lines = f.readlines() suffix = { 12: " 認証 | 権限 |", 13: " ---- | ---- |", 14: " 不要 | 全員 |", 15: " 必要 | 管理者 |", 16: " 必要 | 管理者 |", 17: " 必要 | 本人のみ |", 18: " 必要 | 本人のみ |", 19: " 必要 | 管理者 |", } for ln, suf in suffix.items(): idx = ln - 1 stripped = lines[idx].rstrip("\n") assert stripped.rstrip().endswith("|"), f"line {ln} not a table row: {stripped[:40]!r}" lines[idx] = stripped + suf + "\n" with open(path, "w", encoding="utf-8") as f: f.writelines(lines) PY |
見どころは次のとおりです。
-
with open(...) as f:
with文で開いたファイルは、ブロックを抜けるときに自動的に閉じられます。例外が起きても閉じてくれるので、明示的なclose()を書かなくて済みます。Python でファイルを扱うときの定番の書き方です。 -
encoding="utf-8"を明示する
macOS や Linux ならデフォルトが UTF-8 のことが多いのですが、Windows や一部の環境では別のエンコーディングが使われる場合があります。日本語を含むファイルを扱うときは、encoding="utf-8"を明示しておくと環境依存の事故を避けられます。 -
readlines()は改行を含む
f.readlines()で返るリストの各要素は、末尾に\nを含んだままです。行末に追記する場合は、いったんrstrip("\n")で改行を除去し、追記後に+ "\n"で戻します。 - 行番号は 1-indexed で受け取り、
idx = ln - 1で 0-indexed に
エディタやgrep -nが表示する行番号は 1 から始まりますが、Python のリストは 0 から始まります。人間が指定する値は 1-indexed で受け取り、内部で- 1してから使うと、書き手にも読み手にも自然です。 -
assertで前提を明示する
assert stripped.rstrip().endswith("|")は、書き換える行が表の行であることを事前に確認しています。想定外の行を書き換えるとファイルが壊れるので、前提が崩れたら止まるようにしておきます。ちょい仕事の Python コードでも、こうした最低限の防御は入れる価値があります。エラーメッセージには、次で説明する f-string を使っています。
f-string f”…”
Python の文字列リテラルには、先頭にプレフィックスを付ける書き方があります。よく使われるのが f-string で、
f"..."
の形で書きます。中括弧
{}
の中に変数や式を書くと、その値が埋め込まれた文字列になります。
| 1 2 3 4 | name = "田中" count = 3 print(f"こんにちは、{name}さんの投稿は {count} 件です") # こんにちは、田中さんの投稿は 3 件です |
{}
の中には、変数だけでなく式も書けます。
| 1 2 | items = [1, 2, 3] print(f"合計: {sum(items)}") |
数値のフォーマット指定は、
:
の後ろに書きます。
| 1 2 3 | price = 1234.567 print(f"{price:.2f}") # 1234.57 (小数点以下 2 桁) print(f"{price:,.0f}") # 1,235 (3 桁区切り、整数) |
さらに、
!r
を付けると値の
repr()
が埋め込まれます。文字列のクォートや改行の見え方をそのまま残したいときに便利です。上のコード内で使った
f"line {ln} not a table row: {stripped[:40]!r}"
も、行の内容をそのままの見た目でエラーメッセージに含めるためのものです。
| 1 2 | s = "hello\n" print(f"got: {s!r}") # got: 'hello\n' |
-c とヒアドキュメントの使い分け
-c
は 1〜2 行の処理までが目安です。シェルの引用符の中に Python コードを書くため、複数行のインデントを含む処理は書きにくくなります。文の区切りは
;
を使いますが、
if
や
for
の直後には使えないので、単純な逐次処理に限定されます。
インデントが必要になったらヒアドキュメントに切り替えます。開始タグ
PY
と終了タグ
PY
の間に Python コードをそのまま複数行で書けます。
| 1 2 3 4 5 6 7 | python3 << 'PY' total = 0 for i in range(10): if i % 2 == 0: total += i print("even sum:", total) PY |
開始タグは
'PY'
のようにシングルクォートで囲むのが定番です。囲まないと
$var
などがシェル側で展開されるため、Python コード側に
$
や
`
を含む場合の事故を防げます。
さいごに
これからも、Claude Codeが出力するPythonコードを理解できるようになるために、連載していきたいと思います。