python-debugpy について
Python 開発者向けのデバッグスキルです。ローカルの pdb と、debugpy によるリモートデバッグや実行中プロセスへの接続を扱い、テスト、例外、子プロセスの調査方法も説明します。
主な機能
- 01breakpoint() と pdb によるローカルデバッグ
- 02コードを変更せず pdb でスクリプト起動
- 03debugpy で実行中プロセスに接続
- 04例外発生時や子プロセスの調査
活用シーン
- 01失敗したテストのローカル変数を追跡
- 02長時間稼働プロセスを再起動せず調査
- 03例外発生時の状態を確認
- 04Python 子プロセスのデバッグ
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ nousresearch/ hermes-agent --skill python-debugpyインストール後、Claude Code で/python-debugpyを入力して読み込まれたか確認します。
詳細
Python デバッガー(pdb + debugpy)
概要
状況に応じて使い分ける3つのツールです。
| ツール | 使用する場面 |
|---|---|
breakpoint() + pdb | ローカルでの対話的なデバッグに最適で、最もシンプルです。ソースに breakpoint() を追加して通常どおり実行すると、その行で REPL が起動します。 |
python -m pdb | ソースを編集せずに、既存のスクリプトを pdb で起動します。手早く調べたい場合に便利です。 |
debugpy | リモート/ヘッドレス環境や、「すでに実行中のプロセスへのアタッチ」に使います。DAP で通信し、ターミナルからスクリプトで操作できます。ゲートウェイ、デーモン、PTY の子プロセスなど、長時間実行されるプロセスでも動作します。 |
まずは breakpoint() から始めましょう。 動作する方法の中で、最も手軽です。
使用する場面
- テストが失敗し、トレースバックを見ても値が誤っている理由がわからない
- 関数をステップ実行し、コレクションの変化を確認したい
- 長時間実行されるプロセス(hermes gateway、tui_gateway)が正常に動作せず、再起動できない
- 事後解析:本番環境に近いコードで例外が発生し、クラッシュした箇所のローカル変数を調べたい
- サブプロセス/子プロセス(Python
_SlashWorker、PTY ブリッジワーカー)が実際の不具合箇所である
使用しない場面: print() や logging.debug で1分以内に解決できる問題、または pytest -vv --tb=long --showlocals ですでに原因がわかる問題。
pdb クイックリファレンス
pdb プロンプト((Pdb))で使用します。
| コマンド | 動作 |
|---|---|
h / h cmd | ヘルプを表示 |
n | 次の行へ進む(ステップオーバー) |
s | ステップイン |
r | 現在の関数から戻る |
c | 続行 |
unt N | 行 N まで続行 |
j N | 行 N にジャンプ(同じ関数内のみ) |
l / ll | 現在行付近のソース/関数全体を表示 |
w | 現在位置を表示(スタックトレース) |
u / d | スタック内を上/下に移動 |
a | 現在の関数の引数を表示 |
p expr / pp expr | 式を表示/整形して表示 |
display expr | 停止するたびに expr を自動表示 |
b file:line | ブレークポイントを設定 |
b func | 関数の開始時に停止 |
b file:line, cond | 条件付きブレークポイント |
cl N | ブレークポイント N を解除 |
tbreak file:line | 1回限りのブレークポイント |
!stmt | 任意の Python コードを実行(代入も含む) |
interact | 現在のスコープで完全な Python REPL を起動(終了するには Ctrl+D) |
q | 終了 |
interact コマンドは最も強力です。任意のものをインポートしたり、複雑なオブジェクトを調べたり、状態を変更するメソッドを呼び出したりできます。デフォルトではローカル変数は読み取り専用です。変更するには、(Pdb) プロンプトから !x = 42 を使用します。
レシピ 1:ローカルでブレークポイントを設定
最も簡単な方法です。ファイルを編集します。
def compute(x, y):
result = some_helper(x)
breakpoint() # <-- drops into pdb here
return result + y
コードを通常どおり実行します。breakpoint() の行で停止し、ローカル変数に完全にアクセスできます。
コミットする前に、breakpoint() を削除し忘れないでください。 git diff またはコミット前の grep で確認します。
rg -n 'breakpoint\(\)' --type py
レシピ 2:pdb でスクリプトを起動(ソースの編集なし)
python -m pdb path/to/script.py arg1 arg2
# Lands at first line of script
(Pdb) b path/to/script.py:42
(Pdb) c
レシピ 3:pytest のテストをデバッグ
対話的でない診断には、terminal と標準のテストランナーを使用します。
# Show locals in tracebacks without pdb:
scripts/run_tests.sh tests/path/to/test_file.py --showlocals --tb=long
scripts/run_tests.sh は各ファイルを個別のサブプロセスで実行するため、そこで --pdb や
--trace を指定しても対話プロンプトは利用できません。対話型デバッガーを
使用する場合に限り、レシピ 5 で用意する独立した開発/テスト用インタープリターを
使用します(本番用の生成環境は使用しないでください)。
.venv/bin/python -m pytest tests/foo_test.py::test_bar --pdb
この方法では、隔離環境の保証が適用されません。デバッグには問題ありませんが、プッシュする前にラッパー経由で再実行し、確認してください。
レシピ 4:あらゆる例外を事後解析
import pdb, sys
try:
run_the_thing()
except Exception:
pdb.post_mortem(sys.exc_info()[2])
スクリプト全体をラップする方法もあります。
python -m pdb -c continue script.py
# When it crashes, pdb catches it and you're in the frame of the exception
repl/jupyter でグローバルフックを設定する方法もあります。
import sys
def excepthook(etype, value, tb):
import pdb; pdb.post_mortem(tb)
sys.excepthook = excepthook
レシピ 5:debugpy でリモートデバッグ(実行中のプロセスにアタッチ)
長時間実行されるプロセスの場合に使用します。Hermes gateway、tui_gateway、デーモン、すでに問題が発生していて正常に再起動できないプロセスなどが対象です。
セットアップ
Hermes では、本番環境の稼働中の生成環境ではなく、開発用のチェックアウトとデータホームを分けて使用します。
PM 開発ワークフロー
に従い、そのチェックアウトを有効化します — PowerShell:. .\activate.ps1。宣言済みの
dev エクストラには debugpy が含まれていますが、PM の有効化では同期されません(all には含まれません)。
terminal を通じて、準備済みチェックアウトの Python を使い、新しいデバッグ/テスト環境を呼び出し元の所有下で構築します:
source ./activate
python -m pm.build_env --source . --out .venv --group dev --group test
.venv/bin/python -c "import debugpy; print(debugpy.__file__)"
出力先は事前に存在していてはいけません。再構築する前に、そのプロセスを停止し、使い捨てのこの環境だけを意図的に削除してください。デバッグ対象には、同じ隔離された HERMES_HOME を使います。.venv/bin/python は明示的に構築したこのデバッグ環境であり、推測したアプリケーション用の venv ではありません。以下のパターンはこの環境を通じて実行します。稼働中の本番環境に debugpy を追加しないでください。そこで再現する場合は、すでに準備済みのデバッグ対象を使うか、開発環境での再起動を手配してください。
パターン A:ソースを編集 — 起動時にデバッガーを待機
エントリーポイントの上部付近(またはデバッグしたい関数の内側)に追加します:
import debugpy
debugpy.listen(("127.0.0.1", 5678))
print("debugpy listening on 5678, waiting for client...", flush=True)
debugpy.wait_for_client()
debugpy.breakpoint() # optional: pause immediately once attached
プロセスを起動すると、wait_for_client() で停止します。
パターン B:ソースを編集しない — -m debugpy で起動
.venv/bin/python -m debugpy --listen 127.0.0.1:5678 --wait-for-client your_script.py arg1
モジュールのエントリーポイントの場合も同様です:
.venv/bin/python -m debugpy --listen 127.0.0.1:5678 --wait-for-client -m your.module
パターン C:すでに実行中のプロセスにアタッチ
PID と、対象環境に事前インストールされた debugpy が必要です:
.venv/bin/python -m debugpy --listen 127.0.0.1:5678 --pid <pid>
# debugpy injects itself into the process. Then attach a client as below.
一部のカーネル/セキュリティ設定では、ptrace ベースのインジェクション(/proc/sys/kernel/yama/ptrace_scope)がブロックされます。次の方法で修正します:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
ターミナルからクライアントを接続する
ターミナル側の DAP クライアントとして最も簡単なのは、VS Code CLI または小さなスクリプトです。Hermes 内からは、実用的な方法が 2 つあります:
オプション 1:debugpy 独自の CLI REPL — 公式機能ではありませんが、小さな DAP クライアントスクリプトです:
# ~/.hermes/cache/scratch/dap_client.py
import socket, json, itertools, time, sys
HOST, PORT = "127.0.0.1", 5678
s = socket.create_connection((HOST, PORT))
seq = itertools.count(1)
def send(msg):
msg["seq"] = next(seq)
body = json.dumps(msg).encode()
s.sendall(f"Content-Length: {len(body)}\r\n\r\n".encode() + body)
def recv():
header = b""
while b"\r\n\r\n" not in header:
header += s.recv(1)
length = int(header.decode().split("Content-Length:")[1].split("\r\n")[0].strip())
body = b""
while len(body) < length:
body += s.recv(length - len(body))
return json.loads(body)
send({"type": "request", "command": "initialize", "arguments": {"adapterID": "python"}})
print(recv())
send({"type": "request", "command": "attach", "arguments": {}})
print(recv())
send({"type": "request", "command": "setBreakpoints",
"arguments": {"source": {"path": sys.argv[1]},
"breakpoints": [{"line": int(sys.argv[2])}]}})
print(recv())
send({"type": "request", "command": "configurationDone"})
# ... loop reading events and sending continue/stepIn/etc.
単発の自動化には問題ありませんが、対話型の UX としては使いにくい方法です。
オプション 2:VS Code/Cursor/Zed からアタッチ — いずれかを開いている場合、ユーザーは launch.json を追加できます:
{
"name": "Attach to Hermes",
"type": "debugpy",
"request": "attach",
"connect": { "host": "127.0.0.1", "port": 5678 },
"justMyCode": false,
"pathMappings": [
{ "localRoot": "${workspaceFolder}", "remoteRoot": "<hermes-agent-repo>" }
]
}
オプション 3:DAP をやめて remote-pdb を使う — ターミナルエージェントで本当に必要なのは、通常はこちらです:
独立して管理する Python プロジェクトでは、そのプロジェクトの開発用依存関係に remote-pdb を宣言し、プロジェクトのパッケージマネージャーを通じてデバッグ環境を準備してください。これは Hermes SDK のインストール手順ではありません。Hermes では、宣言済みの debugpy 依存関係を優先してください。以下の remote-pdb の例では、選択したアプリケーション世代に対するインプレースの pip インストールではなく、別途依存関係を宣言し、新たに構築したデバッグ環境が必要です。
コード内では:
from remote_pdb import set_trace
set_trace(host="127.0.0.1", port=4444) # blocks until connection
次に、ターミナルから:
nc 127.0.0.1 4444
# You get a (Pdb) prompt exactly as if debugging locally.
debugpy の DAP プロトコルが過剰な場合は、remote-pdb が最も扱いやすいエージェント向けの選択肢です。IDE との統合が本当に必要な場合にのみ debugpy を使用してください。
Hermes 固有のプロセスのデバッグ
テスト
レシピ 3 を参照してください。ラッパーがサブプロセスの出力をキャプチャするため、対話型の pdb には pytest を直接実行してください。
run_agent.py/CLI — 1 回限りの実行
準備済みのデバッグ用チェックアウトで、問題のある行の近くに breakpoint() を追加し、python hermes を実行します。一時停止した時点で、ターミナルに制御が戻ります。
tui_gateway サブプロセス(hermes --tui が起動)
ゲートウェイは Node TUI の子プロセスとして動作します。方法は次のとおりです:
A. ゲートウェイのソースを編集する:
# tui_gateway/server.py near the top of serve()
import debugpy
debugpy.listen(("127.0.0.1", 5678))
debugpy.wait_for_client()
準備済みのデバッグ用チェックアウトから python hermes --tui を起動します。バックエンドが待機しているため、TUI はフリーズしたように見えます。クライアントをアタッチしてください。continue を実行すると、実行が再開します。デバッグ環境を継承していると決めつける前に、子プロセスのインタープリターとインポートを確認してください。
B. 特定のハンドラーで remote-pdb を使う:
from remote_pdb import set_trace
set_trace(host="127.0.0.1", port=4444) # in the RPC handler you want to trap
TUI から対応するスラッシュコマンドを実行し、別のターミナルで nc 127.0.0.1 4444 を実行します。
_SlashWorker サブプロセス
同じパターンです — ワーカーの exec パス内で set_trace() を使った remote-pdb を使用します。ワーカーはスラッシュコマンド間で維持されるため、最初の呼び出しは接続するまで停止します。それ以降のスラッシュコマンドは、再度待機させない限り通常どおり実行されます。
ゲートウェイ(gateway/run.py)
長時間稼働します。ハンドラーに remote-pdb を使用するか、いずれにせよゲートウェイを再起動する場合は --wait-for-client を指定して debugpy を使用してください。
よくある落とし穴
-
並列実行/出力キャプチャを行うランナーでは、pdb は何も表示せずに動作しなくなります。 プロンプトは表示されず、テストがハングします(pytest-xdist と
scripts/run_tests.shのファイルごとの出力をキャプチャするサブプロセスで発生します)。対話形式でデバッグする場合は、単一ファイルを直接 pytest で実行してください。 -
CI 環境や TTY ではないコンテキストで
breakpoint()を使うと、プロセスがハングします。 ローカルでの使用は安全ですが、決してコミットしないでください。安全策として、コミット前の grep を追加してください。 -
PYTHONBREAKPOINT=0はすべてのbreakpoint()呼び出しを無効にします。ブレークポイントで停止しない場合は、環境変数を確認してください:echo $PYTHONBREAKPOINT -
debugpy.listenがブロックするのは、wait_for_client()も呼び出した場合だけです。 呼び出さない場合は実行が続行され、クライアントの接続前に最初のブレークポイントに到達する可能性があります。 -
強化されたカーネルでは、PID へのアタッチに失敗します。
ptrace_scope=1(Ubuntu のデフォルト)では、同じユーザーによる子プロセスへの ptrace のみが許可されます。回避策として、echo 0 > /proc/sys/kernel/yama/ptrace_scopeを実行する(root 権限が必要)か、最初からdebugpyを使って起動してください。 -
スレッド。
pdbでデバッグできるのは現在のスレッドのみです。マルチスレッドのコードには、debugpy(スレッド対応の DAP)を使用するか、スレッドごとにthreading.settrace()を設定してください。 -
asyncio。
pdbはコルーチンで動作しますが、pdb 内でawaitを使うには Python 3.13 以降が必要です。古いバージョンではinteractモードからawaitを実行してください。3.11/3.12 では、asyncio.run_coroutine_threadsafeを使う方法か、asyncio.ensure_futureを介して!stmtベースの await を実行してください。 -
scripts/run_tests.shは認証情報を削除し、HOME=<tmpdir>を設定します。 ユーザー設定や実際の API キーに依存するバグは、このラッパーでは再現されません。まず生のpytestで再現を確認してから、ラッパーでも再確認してください。 -
フォーク/マルチプロセス。 pdb はフォーク後のプロセスを追跡しません。各子プロセスで独自の
breakpoint()またはset_trace()が必要です。Hermes のサブエージェントでは、一度に 1 つのプロセスをデバッグしてください。
確認チェックリスト
- 独立して構築したデバッグ環境で、次のコマンドを実行して確認します:
.venv/bin/python -c "import debugpy; print(debugpy.__version__); print(debugpy.__file__)" - リモートデバッグでは、ポートが実際にリッスン状態にあることを確認します:
ss -tlnp | grep 5678 - 最初のブレークポイントで実際に停止すること(停止しない場合は、
PYTHONBREAKPOINT=0が設定されている、並列実行/出力キャプチャを行うランナーを使用している、またはアタッチ前に実行が終了している可能性があります) -
where/wで想定したコールスタックが表示されること - デバッグ後のクリーンアップ:コミットしたコードに不要な
breakpoint()/set_trace()が残っていないことrg -n 'breakpoint\(\)|set_trace\(|debugpy\.listen' --type py
すぐに使えるレシピ
「この dict にキーがないのはなぜ?」
# add above the KeyError site
breakpoint()
# then in pdb:
(Pdb) pp d
(Pdb) pp list(d.keys())
(Pdb) w # how did we get here
「このテストは単独では成功するのに、テストスイートでは失敗する。」
scripts/run_tests.sh tests/the_test.py # confirm it fails under the isolated runner first
# For interactive debugging, or if it only fails WITH other tests, use the
# independent development/test interpreter prepared in Recipe 5:
.venv/bin/python -m pytest tests/ -x --pdb
# Now it pdb-traps at the exact failing test after state accumulated.
「非同期ハンドラーがデッドロックする。」
# Add at handler entry
import remote_pdb; remote_pdb.set_trace(host="127.0.0.1", port=4444)
ハンドラーを実行します。nc 127.0.0.1 4444 を実行し、続けて w で中断中のフレームを確認します。!import asyncio; asyncio.all_tasks() で、ほかに保留中のタスクを確認します。
「Ink の子プロセス/サブプロセスで発生したクラッシュの事後解析。」
PYTHONFAULTHANDLER=1 python -m pdb -c continue path/to/entrypoint.py
# On crash, pdb lands at the frame of the exception with full locals