Pythonのsubprocess、バイナリとテキストはどう違う?

出力を取得する指定と、文字列にする指定は別

subprocess.run()で外部コマンドを呼び、その出力をPythonで扱いたい。このとき区別したいのが、出力を取得するかどうかと、取得した出力をどの型で受け取るかだ。capture_output=Trueは標準出力と標準エラーを取得する指定であり、それだけではテキストモードにならない。

公式ドキュメントには、出力を取得する呼び出しとして次のコードがある。ここでは呼び出し部分だけを引用する。

subprocess.run(["ls", "-l", "/dev/null"], capture_output=True)

この指定では、取得した標準出力をCompletedProcess.stdout、標準エラーをCompletedProcess.stderrから参照できる。一方、encodingやerrorsを指定せず、textも真にしていなければ、取得する出力はバイト列になる。

逆に、text=Trueを指定しても、それだけで出力の取得が有効になるわけではない。公式は、取得されていない標準出力や標準エラーの属性値をNoneと説明している。出力が欲しいのか、文字列として扱いたいのかを、それぞれの引数で表す必要がある。

公式が定める既定はバイナリモード

run()の説明には、既定について次の記述がある。

By default, file objects are opened in binary mode.

出典: Python公式ドキュメント「subprocess」(取得日 2026-10-18)

テキストモードになる条件は、encodingまたはerrorsを指定するか、textを真にすることだ。その場合、標準入力・標準出力・標準エラーのファイルオブジェクトは、指定された文字エンコーディングやエラー処理、またはio.TextIOWrapperの既定を使って開かれる。

したがって、text=True自体は、特定の文字エンコーディングを選ぶ指定ではない。文字エンコーディングやエラー処理の指定を省いた場合には、io.TextIOWrapperの既定を使う。特定の文字エンコーディングを指定したい場合は、encodingという引数が用意されている。

また、テキストモードの違いは、受け取る型だけではない。標準入力では、入力中の改行文字'\n'が既定の行区切りであるos.linesepへ変換される。標準出力と標準エラーでは、出力の行末が'\n'へ変換される。バイナリモードでは、文字エンコーディングの変換も改行の変換も行われない。

universal_newlinesはtextと同等の引数で、後方互換性のために提供されている。既存のコードを読む際には、この引数もテキストモードの指定として確認できる。

バイナリとテキストを型と改行で比べる

次の表は、出力を取得した場合の違いを並べたものだ。取得の有無は、モードとは別に設定する。

比べる項目 バイナリモード テキストモード
有効になる条件 テキストモードを有効にする引数を指定しない既定の状態 encodingまたはerrorsを指定するか、textまたは同等のuniversal_newlinesを真にする
CompletedProcessの取得済み出力 バイト列 文字列
run()のinputに渡す値 バイト列 文字列
標準入力の改行 変換しない '\n'をos.linesepへ変換
標準出力・標準エラーの改行 変換しない 行末を'\n'へ変換
文字エンコーディングの変換 行わない 指定値またはio.TextIOWrapperの既定を使用

出力の取得方法にも、別の選択がある。capture_output=Trueでは、内部のPopenに対してstdoutとstderrの両方がPIPEに設定される。この指定と、stdoutやstderr引数の指定は併用できない。

標準出力と標準エラーを一つにまとめて取得したい場合、公式が示す設定はstdout=subprocess.PIPEとstderr=subprocess.STDOUTだ。この場合は両方の出力がCompletedProcess.stdoutに入り、stderr属性はNoneになる。型を選ぶ設定と、出力を分けるかまとめるかの設定を分けて読むと、各引数の役割を整理しやすい。

どちらを使うかと、例外時に残る落とし穴

選ぶ基準は、文字エンコーディングと改行の変換を必要とするかどうかだ。変換せずバイト列として受け取りたい処理には、バイナリモードの規定が対応する。文字列として受け取り、行末を'\n'にそろえて扱いたい処理には、テキストモードの規定が対応する。

標準入力を渡す場合にも、この選択は関係する。run()のinput引数を使うと、内部ではstdinがPIPEに設定されるため、stdin引数を同時には使えない。出力を文字列で受け取る設定を考える際は、入力に渡す値についても同じモードの条件を確認したい。

ただし、text=Trueを指定すれば、どの経路でも出力が文字列になると考えるのは落とし穴だ。公式はTimeoutExpired.outputについて、出力が取得された場合はtext=Trueの設定にかかわらず常にbytesになると記している。TimeoutExpired.stderrについても同じ説明がある。TimeoutExpired.stdoutはoutputの別名だ。

さらに、出力が観測されなかった場合、これらの属性は空のバイト列ではなくNoneのままになることがある。通常の戻り値であるCompletedProcessと、タイムアウト例外に付随する出力は、型の規定を分けて確認する必要がある。

run()でタイムアウトが発生すると、子プロセスは終了させられ、その終了を待ってからTimeoutExpiredが再送出される。ただし、初期のプロセス作成は多くのプラットフォームAPIで中断できないため、指定時間だけで必ず例外を受け取れるとは保証されていない。テキストの変換設定と同様に、タイムアウトにも明示された適用範囲がある。

自分の環境で確かめる手順

次のコードは、公式に掲載されたlsの呼び出しを基に、出力を取得する条件をそろえて既定のバイナリモードとtext=Trueを比較するために構成したものだ。lsと/dev/nullを利用できる環境で実行し、表示された型と出力表現を見比べる。ここには実行結果を掲載しない。

import subprocess

r = subprocess.run(["ls", "-l", "/dev/null"], capture_output=True)
print(type(r.stdout), repr(r.stdout))

r = subprocess.run(
    ["ls", "-l", "/dev/null"],
    capture_output=True,
    text=True,
)
print(type(r.stdout), repr(r.stdout))

呼び出すコマンドとcapture_output=Trueは共通で、後の呼び出しにtext=Trueを加えている。公式の規定では、前の呼び出しで取得した出力はバイト列、後の呼び出しで取得した出力は文字列になる。標準エラーについても、取得した値はr.stderrから参照でき、通常の戻り値では同じモードの条件が適用される。

実行ファイルの指定について、公式は信頼性を高めるために完全修飾パスの使用を推奨している。PATHから名前を検索する場合にはshutil.which()を使う方法が示されている。実行ファイルの探索方法にはプラットフォームによる違いがあるため、上のコードで使うlsも、実行する環境で利用できることが前提になる。

この手順で比較するのは、通常終了時に取得した標準出力だ。タイムアウト例外の出力型や、標準入力の改行変換まで確認したことにはならない。自分のコードで扱う対象が通常の戻り値なのか例外の属性なのかを確認し、それぞれに対応する規定を使って読み取ろう。

コメント

タイトルとURLをコピーしました