Claude Codeでjupytextを使い、ipynbの差分を.pyで読む
jupytextで.ipynbと.py(percent形式)をペアにし、Claude Codeには.py側を編集させてsyncで戻す手順です。差分レビューの流れと、セル順序・出力が崩れていないかの確かめ方をまとめました。
.ipynbをそのままGitに入れると、PRの差分はJSONの断片になります。1行直しただけでも、execution_countや出力のbase64が一緒に動き、どこが本当の変更か分かりません。jupytextで.ipynbと.py(percent形式)をペアにし、Claude Codeには.py側を編集させ、レビューも.pyの差分で行う。これが本記事の手順です。
必要なのは、ペアリングの設定、Claude Codeに渡す約束事(CLAUDE.mdと権限ルール)、そして変換後に崩れていないことの確認の3つです。セル単位の編集ツールNotebookEditの使い方はClaude CodeでJupyter Notebookを編集するNotebookEditの使い方にあります。ここでは.ipynbを直接触らず、テキスト側に寄せる運用を扱います。
ペアリングで.ipynbの差分が読めるようになる仕組み
jupytextのペアノートブックは、.ipynbとテキスト形式のファイルを同時に保持します。jupytextのドキュメントでは、開くときに入力(コード)は新しいほうのファイルから、出力は.ipynbから読み込まれ、保存するとペアの全ファイルが更新されます。Gitに入れるのは.pyだけでもよく、共同作業者が.pyを取り込んでJupyterで開いて保存すれば、.ipynbが更新または再生成されます。
percent形式は、すべてのセルを# %%というコメントで区切る形式です。markdownセルは# %% [markdown]で始まり、本文は#付きのコメントになります。VS CodeやSpyderなど、# %%を解釈するエディターでもセルとして扱えます。
次の例は、jupytext 1.19.6(Python 3.12)で、markdown1つとコード2つの小さなノートブックをpy:percentでペアにしたときの.pyです。
# ---
# jupyter:
# jupytext:
# formats: ipynb,py:percent
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.19.6
# kernelspec:
# display_name: Python 3
# language: python
# name: python3
# ---
# %% [markdown]
# # 売上集計
# 月別に集計する
# %%
import pandas as pd
df = pd.DataFrame({'m':[1,2],'v':[10,20]})
# %%
total = df['v'].sum()
print(total)先頭のYAMLヘッダーにはペアの設定とkernel情報が入ります。出力(30と表示された結果)は.pyに含まれません。出力は.ipynbだけに残るため、.pyの差分は「コードとmarkdownの変更」だけで構成されます。
ペアリングの設定は2通りある
全ノートブックに一括で適用する方法と、1つずつ設定する方法があります。
プロジェクト直下にjupytext.tomlを置くと、そのフォルダー以下のノートブックがまとめてペアになります。
formats = "ipynb,py:percent"pyproject.tomlの[tool.jupytext]に書くこともできます。.ipynbをnotebooks/、.pyをscripts/に分けたいときは、[formats]テーブルでフォルダーの対応を指定します。
1つずつ設定するなら、次のコマンドです。
jupytext --set-formats ipynb,py:percent sales.ipynb実行すると.ipynbのメタデータに"jupytext": {"formats": "ipynb,py:percent"}が書き込まれ、同じ名前のsales.pyが生成されます。個別のペア設定は全体設定より優先されます。特定のノートブックだけペアを外したいときは、--set-formats ipynbと形式を1つだけ指定します。
落とし穴
ペア設定のないファイルにsyncをかけても、何も作られない
jupytext.tomlを置いたフォルダーなら、ペア情報のない.ipynbにjupytext --syncをかけるだけで、対応する.pyが作られました(1.19.6での挙動)。逆に、設定の対象外にある.pyを渡すと、plain.py is not a paired notebookという警告が出て、終了コードは0のままでした。CIやhookに入れるときは、終了コードだけで判断せず、警告文もログで拾えるようにしておくと安心です。
Claude Codeに渡す約束事を先に書く
ここからがClaude Code側の設定です。狙いは2つで、Claudeに.ipynbを直接書き換えさせないことと、.pyを直したら必ず--syncで戻させることです。
.ipynbを読ませると出力ごと入る
Claude CodeのReadツールは.ipynbを開くと、全セルを出力付きで返します。tools-referenceには、セルの合計が256KBを超えるか、トークン上限を超えると、内容の代わりにエラーを返すとあります。画像出力や長い表出力を含むノートブックは、.ipynbのまま読ませるだけでコンテキストを圧迫しかねません。.pyは出力を持たないので、コードの意図を読ませる用途にも向きます。
書き換えのほうは、NotebookEditがcell_idでセルを1つずつ変更します。通常のEditのような文字列置換ではありません。セル単位の変更は確実ですが、PRに出てくる差分は結局JSONです。レビューを.pyで行うなら、編集も.pyで行わせるほうが、見るものと直すものが揃います。
CLAUDE.mdの規約断片
プロジェクトのCLAUDE.mdに、次のような規約を置きます。内容は一例で、フォルダー名は自分の構成に合わせて読み替えてください。
## ノートブック
- notebooks/ 配下の .ipynb は jupytext でペア済み(formats = ipynb,py:percent)。
- 編集は .py 側で行い、.ipynb は直接 Edit しない。
- .py を編集したら、同じターンの最後に `jupytext --sync <対応する .ipynb>` を実行する。
- sync の後に `git diff --stat` を実行し、変更ファイルが .py と .ipynb の2つだけであることを確認する。権限ルールで、必要なコマンドだけ通す
jupytextをBash経由で何度も実行させるなら、権限ルールで許可しておくと承認待ちが減ります。Bash(...)のルールはワイルドカードの*をどこにでも置けます。permissionsページの表では、Bash(npm run *)がnpm run buildにもnpm run test --watchにも一致し、npm installには一致しません。
{
"permissions": {
"allow": [
"Bash(jupytext --sync *)",
"Bash(jupytext --test *)",
"Bash(git diff *)"
]
}
}Edit(path)のルールはNotebookEditにも効きます。permissionsページには「Edit(notebooks/**)は、そのフォルダーのファイルへのNotebookEdit呼び出しも対象にする」とあります。.ipynbの直接編集を止めたいなら、denyにEdit(notebooks/**/*.ipynb)を足す方法があります。ただしこのルールはEdit系のツールが対象です。Bashで実行するjupytextの書き込みまで止めるものではないので、--syncの許可は別にBash(...)で与えます。
.py側で編集させ、syncで戻す流れ
Claude Codeに頼むときの指示は、次のように「どのファイルを直し、どう戻すか」まで書いておきます。
notebooks/sales.py の集計セルを修正して、合計に消費税10%を足してください。
編集は .py で行い、終わったら jupytext --sync notebooks/sales.ipynb を実行。
その後 jupytext --test --to py:percent notebooks/sales.ipynb で往復変換を確認し、
git diff --stat の結果を報告してください。.pyを直して.ipynbに反映するまで
- 1
Claudeが.pyを編集する
通常のEditツールで
# %%区切りのテキストを直します。セルの境界は# %%の行なので、その行を消したり動かしたりしない指示を添えると安全です。 - 2
syncで.ipynbを更新する
jupytext --sync sales.ipynbは、新しい.pyの入力を読み込んで.ipynbのコードセルを更新し、既存の出力は残します。 - 3
往復変換を確認する
jupytext --test --to py:percent sales.ipynbが、変換して戻したときにノートブックが変わらないかを調べます。 - 4
差分を.pyで読む
git diffの対象は.pyです。.ipynbの差分は、変更行数だけ見れば足ります。
Claude Codeは、ツール呼び出しのたびにファイルを読み直します。common-workflowsのページには、別のアプリで編集した内容を、次にClaudeがそのファイルを読むときに拾う、という説明があります。Jupyterを開いたまま.pyをClaudeに直させる運用ができるのはこのためです。ただし、Jupyter側の自動保存が.pyを上書きしないように、あとの節で説明する注意が要ります。
編集のたびにsyncを忘れさせたくないときは、hookが使えます。hooks-guideの例はPostToolUseイベントにEdit|Writeのmatcherを置いてPrettierを走らせる形です。同じ形で、編集されたファイルがnotebooks/配下の.pyのときだけjupytextを呼べます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | grep -E '/notebooks/.*\\.py$' | xargs -r jupytext --sync"
}
]
}
]
}
}対象をnotebooks/配下に絞っているのは、プロジェクト直下のjupytext.tomlの範囲内なら、ペア情報のない.pyにもsyncで.ipynbが作られるためです。絞らないと、src/utils.pyのような通常のモジュールを編集しただけで余計な.ipynbができるおそれがあります。grepで絞った後にxargs -rで空入力を弾くので、対象外のファイルの編集では何も実行されません。-rは手元のmacOS(BSD版のxargs)でも受け付けられ、空入力では何も実行されませんでした。設定後は/hooksで登録されたことを確認できます。hookを使わず、CLAUDE.mdの約束事とレビュー時のgit diff --statだけで運用する手もあります。
変換後にセル順序と出力が崩れていないか確かめる
ペアリングは便利ですが、「.pyを直したら.ipynbのどこが変わったか」は、見る観点を決めておかないと検証が曖昧になります。確認は3段に分けます。
1. 往復変換が安定しているか
jupytextの--testは、.ipynbから.pyへ変換し、また戻したときに内容が保たれるかを調べるコマンドです。
# ipynb -> py:percent -> ipynb の往復
jupytext --test --to py:percent sales.ipynb
# ペア(py:percent と ipynb を更新しながら)での往復
jupytext --test --update --to py:percent sales.ipynb差異があれば報告され、なければ何も出さずに終わります。上のサンプルでは終了コード0でした。jupytextの比較は「jupytextが重要と見なす項目」についての比較です。メタデータの細部まで厳密に比べたいときは--test-strictを使い、-xを付けると最初の差異を詳しく表示します。
2. セルの数・種類・順序
.pyの差分で最も見落としやすいのが、セルの順序の入れ替えです。コードの行そのものは動いていないのに、# %%ブロックの位置が変わっただけで、ノートブックの実行結果は変わりえます。sync後に、セルの種類と出力数を並べて見ます。
python3 -c "import json,sys; nb=json.load(open(sys.argv[1])); \
print([(c['cell_type'], len(c.get('outputs', []))) for c in nb['cells']])" sales.ipynb先ほどのサンプルでは、次のように出ました。
[('markdown', 0), ('code', 0), ('code', 1)]同じ操作を、Claudeの実行前後で行い、種類の並びが意図通りかを比べます。次に、.ipynb側でセルを入れ替えて--syncした場合の.pyの差分です。コード行は一字も変わっていないのに、2つのセルがそっくり入れ替わった形で差分に出ます。
# %%
-import pandas as pd
-df = pd.DataFrame({'m':[1,2],'v':[10,20]})
+total = df['v'].sum()
+print(total)
# %%
-total = df['v'].sum()
-print(total)
+import pandas as pd
+df = pd.DataFrame({'m':[1,2],'v':[10,20]})この差分が出たら、dfを作るセルが後ろに回っているので、上から順に実行すると失敗します。.ipynbのJSON差分では、同じ事故が数十行の移動に埋もれて見えにくくなります。
3. 出力は「古いまま残る」ことがある
ここは見落としやすい点です。.pyのコードを書き換えて--syncすると、.ipynbのコードは更新されますが、出力は更新前のものが残ります。サンプルでprint(total)をprint(f"total={total}")に変えて--syncしたあとも、そのセルの出力数は1のままでした(中身は30)。コードが変わったのに、出力は実行されていないので前の値です。
つまり、PRに載る.ipynbの出力は、そのコードで実行した結果とは限りません。出力を信頼できる状態にしたいなら、Jupyterで再実行して保存するか、.pyから作り直すときにjupytext --to notebook --executeで実行まで一度に行う方法があります。出力をGitに残さない運用なら、出力を剥がすツールと組み合わせる手もあります。nbstripoutは、ノートブックを読み込んで出力と一部のメタデータを除去して書き戻すツールです。
もう一つの落とし穴は、出力を持たない変換です。jupytext --to notebook sales.pyのように--updateなしで.pyから.ipynbを作ると、出力のない新しいノートブックになります。出力を残したまま入力だけ更新したいときは--update --to notebookを使います。サンプルでは、前者で出力が0になりました。
差分レビューの進め方
ペアの運用が回りはじめると、レビューで見るのは.pyの差分です。PRに.pyと.ipynbの両方が載る運用では、見る順序を決めておくと迷いません。
.pyの差分で、コードとmarkdownの変更を読む。ここが本体の変更です。.ipynbの差分は、行数と、セル数・順序の確認結果で見る。JSONの中身を逐一読まない。- 出力が古いままの可能性があるときは、レビュー前にJupyterで再実行して保存する。
Claude Codeにレビューを手伝わせるなら、git diff -- '*.py'の結果を渡して、「セルの順序に依存する変更がないか」「# %%行が削除・移動されていないか」を指示に入れます。差分の読み方を観点で固定する書き方は、Claude Codeのhelm diffレビューでも同じ考え方を使っています。別の文脈を持つサブエージェントに差分を検証させる設計は、敵対的レビューが扱っています。
つまずきやすいところ
JupyterとClaudeが同時に保存する
jupytextのペアノートブックのページでは、ペアのノートブックをJupyterで開いたまま、テキストエディターで編集してよいとしています。ただし、二つが同時に保存しないように、Jupyterの自動保存を切り、エディターから戻ったらJupyterでノートブックを再読み込みするよう勧めています。Claude Codeが.pyを編集する場合も同じです。再読み込みを忘れて保存すると、Jupyterは、どちらのバージョンを残すかを尋ねます。新しいほうを選ぶのが基本で、迷ったら.pyのコピーを退避してから上書きし、手で突き合わせます。
コミット時にペアを揃える
人間が.ipynbだけを更新してコミットすると、.pyが古いままになります。jupytextはpre-commitフック用の定義を持っており、entryはjupytext --pre-commit-mode、対象はjupyter・markdown・pythonのファイルです。これを入れると、コミット時にペアの同期が走ります。Claude Codeにコミットまで任せる運用でも、フックが最終的な揃えを担保します。
percent形式に収まらないもの
percent形式は、セルのメタデータ(# %% key="value")とタイトルを持てます。一方、ノートブックのメタデータはYAMLヘッダーに入る項目が限られ、一部のセルメタデータは.pyに出ません。Sphinx-galleryのスクリプトのように別形式として判定されるファイルもあります。往復で失われる情報があるかは、手元のノートブックで--testを実行して確かめます。
まとめ
jupytextのペアにすると、レビューの対象を「JSONの差分」から「.pyのコード差分」に移せます。ただし出力は.ipynbにしか残らず、.pyを直しても自動では更新されません。Claude Codeには.pyを直させて--syncで戻させ、セルの数と順序、出力の鮮度を別に確かめる。この分担にすると、差分が読めることと、ノートブックが壊れていないことを両立できます。
最初の1本は、出力が重くない小さなノートブックで試すのが無難です。--testが通り、--syncの前後でgit diff --statが.pyと.ipynbの2ファイルだけを示すなら、運用に載せられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。