Dari dalam Python, sering kali kita perlu menjalankan program lain: git untuk membaca status repositori, ffmpeg untuk mengonversi video, atau skrip Python kedua sebagai tool terpisah. Module subprocess dari standard library adalah jembatan resmi antara proses Python kita dan proses eksternal. Pada Chapter ini kita menjalankan perintah, menangkap outputnya, menangani kegagalan dan timeout, sampai menghubungkan dua proses dengan pipe.

subprocess.run dan CompletedProcess #

Fungsi utama module ini adalah subprocess.run(): jalankan satu perintah, tunggu sampai selesai, lalu kembalikan object CompletedProcess. Berkas run_dasar.py:

python
import subprocess

hasil = subprocess.run(["python3", "-V"], capture_output=True, text=True)

print("returncode:", hasil.returncode)
print("stdout    :", hasil.stdout.strip())
print("stderr    :", repr(hasil.stderr))

Output yang Dihasilkan:

text
returncode: 0
stdout    : Python 3.12.15
stderr    : ''

Mari kita bedah baris-baris pentingnya:

  1. Baris 3 (["python3", "-V"]): perintah ditulis sebagai list — nama program diikuti setiap argumennya sebagai elemen terpisah, tanpa shell yang ikut menafsirkan.
  2. Baris 3 (capture_output=True): output stdout dan stderr anak ditampung ke memory sehingga bisa dibaca lewat hasil.stdout dan hasil.stderr; tanpa parameter ini, output langsung mengalir ke terminal.
  3. Baris 3 (text=True): hasil dibuka sebagai string, bukan bytes; tanpa parameter ini hasil.stdout berupa bytes yang harus didekode lebih dulu.

Contoh cepat lainnya dengan echo:

python
import subprocess

hasil = subprocess.run(["echo", "Halo dari subprocess"], capture_output=True, text=True)
print(hasil.stdout, end="")

Output yang Dihasilkan:

text
Halo dari subprocess

Object CompletedProcess menyimpan empat atribut: args (perintah yang dijalankan), returncode (kode keluar anak), stdout, dan stderr.

check=True dan CalledProcessError #

Secara default subprocess.run() tidak peduli apakah perintah gagal; yang berubah hanya returncode. Parameter check=True mengubah perilaku itu: bila exit code bukan nol, Python memicu exception CalledProcessError. Berkas run_check.py:

python
import subprocess

perintah = ["python3", "-c", "import sys; sys.exit(3)"]

hasil = subprocess.run(perintah, capture_output=True, text=True)
print("tanpa check, returncode:", hasil.returncode)

try:
    subprocess.run(perintah, check=True, capture_output=True)
except subprocess.CalledProcessError as galat:
    print("CalledProcessError, returncode:", galat.returncode)

Output yang Dihasilkan:

text
tanpa check, returncode: 3
CalledProcessError, returncode: 3

Exception CalledProcessError membawa galat.cmd, galat.returncode, galat.output, dan galat.stderr sehingga penangan galat punya semua konteks. Gunakan check=True setiap kali kegagalan anak berarti kegagalan program kita — kebiasaan yang membuat alur kontrol jujur.

shell=False Selalu #

Parameter shell=True menjalankan perintah melalui shell sistem, yang berarti karakter seperti ;, |, dan $(...) ditafsirkan sebagai sintaks shell. Gabungkan ini dengan input dari pengguna dan kita mendapatkan celah command injection. Simulasi berikut menunjukkan mengapa mode aman (shell=False, yang merupakan default) benar-benar aman:

python
import subprocess

nama_berkas = "laporan.txt; rm -rf /"  # input berbahaya dari pengguna

# shell=False: string tetap satu argumen, tidak ditafsirkan shell
hasil = subprocess.run(["ls", "-l", nama_berkas], capture_output=True, text=True)
print("returncode:", hasil.returncode)
print("perintah tambahan tidak pernah dijalankan")

Output yang Dihasilkan:

text
returncode: 2
perintah tambahan tidak pernah dijalankan

Dengan shell=False, string "laporan.txt; rm -rf /" utuh diperlakukan sebagai satu nama berkas (yang memang tidak ada, sehingga ls keluar dengan kode 2). Bagian rm -rf / tidak pernah dieksekusi karena tidak ada shell yang memecah kalimat tersebut.

Membatasi Waktu dengan timeout #

Proses eksternal bisa menggantung: server tidak menjawab, atau berkas korup membuat tool berputar selamanya. Parameter timeout= (satuan detik) membunuh anak yang melewati batas waktu dan memicu exception TimeoutExpired. Berkas run_timeout.py:

python
import subprocess

try:
    subprocess.run(["sleep", "5"], timeout=0.3, capture_output=True)
except subprocess.TimeoutExpired as galat:
    print("TimeoutExpired setelah", galat.timeout, "detik")

Output yang Dihasilkan:

text
TimeoutExpired setelah 0.3 detik

Exception ini membawa galat.cmd dan galat.timeout sehingga log kesalahan menyebut perintah mana yang menggantung dan berapa lama batasnya.

Popen dan Pipe antar Proses #

subprocess.run() menutup semua kebutuhan eksekusi tunggal. Class Popen adalah mesin di baliknya untuk kasus lanjutan: streaming output sambil anak masih berjalan, atau menghubungkan dua proses. Contoh berikut mempipe output proses_satu menjadi input proses_dua, persis seperti program1 | program2 di shell:

python
from subprocess import PIPE, Popen

proses_satu = Popen(
    ["python3", "-c", "print('jeruk')\nprint('apel')"],
    stdout=PIPE,
    text=True,
)

proses_dua = Popen(
    ["python3", "-c", "import sys; print('total baris:', sys.stdin.read().count(chr(10)))"],
    stdin=proses_satu.stdout,
    stdout=PIPE,
    text=True,
)

proses_satu.stdout.close()
keluaran, _ = proses_dua.communicate()

print("hasil pipe:", keluaran.strip())

Output yang Dihasilkan:

text
hasil pipe: total baris: 2

Baris stdin=proses_satu.stdout menyambungkan stdout anak pertama ke stdin anak kedua. Baris proses_satu.stdout.close() memastikan anak kedua menerima tanda akhir input (EOF) setelah anak pertama selesai, dan communicate() membaca output sambil menunggu proses berakhir.

Panduan memilih cepat:

KebutuhanAlat
jalankan satu perintah, tunggu hasilsubprocess.run()
baca output baris demi baris sambil bekerjaPopen + iterasi stdout
sambungkan dua programPopen + stdout=PIPE

Dengan bekal run(), check=True, timeout, dan pipe, kita siap masuk ke resep praktis menjalankan program eksternal di dunia nyata pada Chapter Menjalankan Program Eksternal.