Python subprocess.Popenの使い方|run()との違い・stdout・communicate・timeout
Pythonのsubprocess.Popenは、外部コマンドを起動したらすぐに制御を返し、実行中のプロセスに対して出力の逐次読み取り・入力・待機・停止ができるクラスです。終わるまで待って結果をまとめて受け取るだけならsubprocess.run()で足り、Popenは「待たずに動かしたい」「1行ずつ読みたい」「途中で止めたい」ときに使います。
この記事では、Popenの基本(poll・wait)、communicateでのstdout・stderrの受け取り方、出力をリアルタイムに読む方法、timeout・terminate・kill、shell=True、パイプでのつなぎ方、そしてrun()との違いを、実行結果つきで説明します。コードはmacOS上のPython 3.12で実行しました(lsやsleepなどのコマンドはmacOS・Linuxのものです。Windowsでは別のコマンドに置き換えてください)。
subprocess.Popenとrun()の違い
Pythonの公式ドキュメントは、サブプロセスの起動にはまずrun()を使い、それで扱えない高度な場合に土台のPopenを直接使うよう案内しています(subprocess — サブプロセス管理)。run()も内部ではPopenを使っています。
| やりたいこと | 使うもの |
|---|---|
| コマンドを実行して、終わったら結果をまとめて受け取る | run() |
| 終了コードが0以外ならエラーにしたい | run(check=True) |
| 起動したまま、Python側で別の処理を続けたい | Popen |
| 出力を1行ずつリアルタイムに処理したい | Popen |
| 複数のコマンドを同時に動かしたい | Popen |
| 実行中のプロセスを途中で止めたい | Popen |
| コマンド同士をパイプでつなぎたい | Popen |
run()の使い方はPython subprocess.run()とは?外部コマンドを実行する方法で解説しています。この記事はPopenで初めてできることに絞ります。
Popenの基本:起動するとすぐ戻る(poll・wait)
import subprocess
p = subprocess.Popen(["sleep", "0.3"])
print("起動直後 poll():", p.poll())
p.wait()
print("wait後 poll():", p.poll(), "/ returncode:", p.returncode)
実行結果:
起動直後 poll(): None
wait後 poll(): 0 / returncode: 0
| メソッド・属性 | 動き |
|---|---|
poll() |
終わっていれば終了コード、実行中ならNoneを返す。待たない |
wait() |
終わるまで待ち、終了コードを返す。timeout=も指定できる |
returncode |
終了コード。終わる前はNone |
pid |
起動したプロセスのID |
poll()のNoneは「まだ動いている」、0は「正常に終わった」です。if not p.poll():のように書くと両方が同じ扱いになってしまうので、if p.poll() is None:と書いてください。
存在しないコマンドを渡すと、起動の時点でFileNotFoundError: [Errno 2] No such file or directory: 'no-such-command'になります。
stdout・stderrを受け取る:communicate()
出力を受け取るにはstdout=subprocess.PIPE(エラー出力も欲しければstderr=subprocess.PIPE)を指定し、communicate()で読みます。text=Trueを付けると文字列(str)で、付けないとバイト列(bytes)で返ります。
import subprocess
p = subprocess.Popen(["ls", "/no/such/dir"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
out, err = p.communicate()
print("stdout:", repr(out))
print("stderr:", err.strip())
print("returncode:", p.returncode)
実行結果:
stdout: ''
stderr: ls: /no/such/dir: No such file or directory
returncode: 1
communicate()は終わるまで待ってから、標準出力と標準エラーをタプルで返します。PIPEを指定しなかったほうはNoneになります。
stderrをstdoutにまとめる
import subprocess
p = subprocess.Popen(["ls", "/no/such/dir"], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True)
out, err = p.communicate()
print("out:", out.strip())
print("err:", err)
実行結果:
out: ls: /no/such/dir: No such file or directory
err: None
ログに出る順番どおりにまとめて読みたいときはstderr=subprocess.STDOUTを使います。
communicate()は1回しか呼べない
同じプロセスに2回communicate()を呼ぶと、1回目でストリームが閉じているためValueError: I/O operation on closed file.になります。結果は1回目で変数に入れておいてください。
stdoutを1行ずつリアルタイムに読む
Popenを使う大きな理由の1つです。p.stdoutをforで回すと、コマンドが出力した行から順に受け取れます。
import subprocess
cmd = ["sh", "-c", "for i in 0 1 2; do echo step $i; sleep 0.2; done"]
with subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) as p:
for line in p.stdout:
print("受信:", line.rstrip())
print("returncode:", p.returncode)
実行結果(0.2秒ごとに1行ずつ表示されます):
受信: step 0
受信: step 1
受信: step 2
returncode: 0
長時間動くコマンドの進捗を表示したいときや、巨大な出力を一度にメモリへ載せたくないときに使います。run()では、終わるまで何も受け取れません。
なお、呼び出す側がPythonスクリプトの場合、出力がバッファにたまってまとめて届くことがあります。そのときは呼び出すコマンドをpython3 -uで起動するか、子のスクリプトでprint(..., flush=True)にしてください。
標準入力にデータを渡す
import subprocess
p = subprocess.Popen(["wc", "-l"], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, text=True)
out, _ = p.communicate("x\ny\nz\n")
print("行数:", out.strip())
実行結果:
行数: 3
stdin=subprocess.PIPEを指定し、communicate()の引数に渡したい文字列を入れます。
wait()とPIPEの組み合わせはデッドロックする
公式ドキュメントが警告している落とし穴です。stdout=PIPEを指定したままwait()を先に呼ぶと、出力の多いコマンドで固まることがあります。
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
p.wait() # 出力が多いと固まる
out = p.stdout.read()
OSのパイプには容量の上限があり、いっぱいになるとコマンド側は「読まれるのを待つ」状態になります。Python側は「終わるのを待つ」ので、お互いに待ち続けます。PIPEを使うときはwait()ではなくcommunicate()を使うよう、ドキュメントでも案内されています。
timeoutを指定する
import subprocess
p = subprocess.Popen(["sleep", "5"], stdout=subprocess.PIPE, text=True)
try:
out, _ = p.communicate(timeout=0.5)
except subprocess.TimeoutExpired:
p.kill()
out, _ = p.communicate()
print("タイムアウトしたので停止。returncode:", p.returncode)
実行結果:
タイムアウトしたので停止。returncode: -9
communicate(timeout=秒)は時間切れでTimeoutExpiredを出しますが、プロセス自体は止まりません。kill()で止めたあと、もう一度communicate()を呼んで後片付けをするのが公式ドキュメントの書き方です。
プロセスを止める:terminate()とkill()
import subprocess
p = subprocess.Popen(["sleep", "10"])
p.terminate()
p.wait()
print("terminate:", p.returncode)
p = subprocess.Popen(["sleep", "10"])
p.kill()
p.wait()
print("kill:", p.returncode)
実行結果:
terminate: -15
kill: -9
macOS・Linuxでは、シグナルで止まったプロセスのreturncodeはマイナスのシグナル番号になります。terminate()はSIGTERM(15)で終了を求め、kill()はSIGKILL(9)で強制終了します。まずterminate()、止まらなければkill()の順が基本です。
複数のコマンドを同時に動かす
import subprocess, time
t = time.perf_counter()
ps = [subprocess.Popen(["sleep", "0.3"]) for _ in range(3)]
for p in ps:
p.wait()
print(f"Popenで3つ同時: {time.perf_counter() - t:.2f}秒")
t = time.perf_counter()
for _ in range(3):
subprocess.run(["sleep", "0.3"])
print(f"run()で3つ順番: {time.perf_counter() - t:.2f}秒")
実行結果:
Popenで3つ同時: 0.31秒
run()で3つ順番: 0.93秒
先に全部起動してから、まとめてwait()するのが並行実行の基本形です。待ち時間の長いコマンド(変換処理や通信)をいくつも動かすときに効きます。
パイプでつなぐ(コマンド | コマンド)
import subprocess
p1 = subprocess.Popen(["printf", "b\na\nb\n"], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["sort", "-u"], stdin=p1.stdout,
stdout=subprocess.PIPE, text=True)
p1.stdout.close()
print(repr(p2.communicate()[0]))
実行結果:
'a\nb\n'
シェルのprintf ... | sort -uと同じです。p1.stdout.close()は、p2が先に終わったときにp1が終了を知るために必要で、公式ドキュメントの例にも入っています。
shell=Trueの使い方と注意点
import subprocess
p = subprocess.Popen("echo hello world | wc -w", shell=True,
stdout=subprocess.PIPE, text=True)
print("単語数:", p.communicate()[0].strip())
実行結果:
単語数: 2
shell=Trueにすると、コマンドを1つの文字列で渡し、パイプ(|)や環境変数をシェルに解釈させられます。手軽ですが、ユーザー入力など外から来た文字列を混ぜると、意図しないコマンドを実行される危険があります。公式ドキュメントの「セキュリティで考慮すべき点」でも注意されています。外部の値を使うときはリストで渡し、shell=Trueは避けてください。
with文で確実に後片付けする
Popenはwith文に対応しており、ブロックを抜けると標準入出力が閉じられ、プロセスの終了を待ちます。上の「1行ずつ読む」例のように、Popenを直接使うときはwithを習慣にすると閉じ忘れ・待ち忘れを防げます。
os.popen()との違い
import os, subprocess
print(repr(os.popen("echo hello").read()))
print(repr(subprocess.run(["echo", "hello"], capture_output=True, text=True).stdout))
実行結果:
'hello\n'
'hello\n'
「popen python」で検索するとos.popen()も出てきます。os.popen()はシェル経由で実行して出力を読むだけの簡単な関数で、終了コードの扱いやstderrの分離などはできません。公式ドキュメントにも、os.popen()をsubprocessに置き換える書き方の節があります。新しく書くコードではsubprocessを使ってください。
よくある質問
Q. subprocess.Popenとrun()はどちらを使えばよいですか?
終わるまで待って結果を受け取るだけならrun()を使います。待たずに別の処理を続けたい、出力を1行ずつ読みたい、途中で止めたい、複数を同時に動かしたいときにPopenを使います。
Q. poll()がNoneを返すのはなぜですか?
プロセスがまだ実行中だからです。終了すると終了コード(正常終了なら0)を返します。判定はif p.poll() is None:のように書きます。
Q. stdoutをリアルタイムに取得するには?
stdout=subprocess.PIPEとtext=Trueを指定して起動し、for line in p.stdout:で1行ずつ読みます。子がPythonなら-uオプションかflush=Trueで出力のため込みを防ぎます。
Q. wait()で固まってしまうのはなぜですか?
stdout=PIPEを指定したまま出力が多いと、パイプがいっぱいになって互いに待ち続けるためです。PIPEを使うときはwait()ではなくcommunicate()を使います。
まとめ
- Popenは起動後すぐに戻る。
poll()は実行中ならNone、wait()で終了を待つ - 出力は
stdout=PIPE・stderr=PIPEとcommunicate()で受け取る(1回だけ) for line in p.stdoutでリアルタイムに1行ずつ読める- PIPEと
wait()の組み合わせはデッドロックの原因。communicate()を使う timeoutのあとはkill()してから再度communicate()- 外部の文字列を使うときは
shell=Trueを避ける - 結果をまとめて受け取るだけなら
run()で十分

