Skrip Python kamu perlu meresize ribuan foto, memanggil git, atau menjalankan ffmpeg. Kamu bisa buka terminal dan ketik satu per satu, atau biarkan Python yang mengerjakannya. Modul subprocess ada di pustaka standar untuk urusan ini: memanggil program eksternal, membaca hasilnya, dan mengontrol cara proses berjalan. Hampir semua skrip otomasi yang saya tulis akhirnya menyentuh subprocess, entah untuk backup, konversi file, atau cek status server.

Kenapa subprocess, bukan os.system()

Kode lama sering pakai os.system("ls -l"). Itu masih jalan, tapi cuma mengembalikan status berupa angka dan tidak bisa menangkap output ke dalam variabel. Mau parsing hasilnya? Redirect ke file dulu, baca lagi, lalu hapus. subprocess.run() jauh lebih kaya: dia mengembalikan objek CompletedProcess yang membawa returncode, stdout, dan stderr sekaligus, plus opsi timeout dan kontrol environment. Dokumentasi resmi Python sendiri mengarahkan kamu ke subprocess ketimbang os.system().

Menjalankan perintah sederhana

Bentuk paling dasar memakai daftar argumen, bukan satu string panjang. Ini penting: subprocess menjalankan perintah langsung tanpa shell kalau kamu kasih list, jadi tidak ada celah injeksi dan tidak ada masalah quoting.

python
import subprocess

subprocess.run(["ls", "-l"])

Perhatikan ls dan -l menjadi dua elemen terpisah. Kalau argumenmu mengandung spasi — misalnya nama file "laporan bulanan.pdf" — masukkan saja sebagai satu elemen list, dan subprocess yang mengurus escaping-nya. Tidak perlu pusing dengan tanda kutip berlapis.

Menangkap output

Mau pakai hasil perintahnya di dalam kode? Tambah capture_output=True supaya stdout dan stderr masuk ke variabel, lalu text=True supaya hasilnya berupa string, bukan bytes.

python
import subprocess

hasil = subprocess.run(
    ["git", "status", "--short"],
    capture_output=True,
    text=True,
)

print("STDOUT:", hasil.stdout)
print("STDERR:", hasil.stderr)
print("Kode keluar:", hasil.returncode)

Tanpa text=True, stdout dan stderr berupa bytes. Kamu bisa decode manual lewat hasil.stdout.decode(), tapi text=True lebih rapi karena otomatis memakai encoding default sistem. Kalau butuh encoding spesifik, lewati text=True dan decode sendiri dengan hasil.stdout.decode('utf-8').

Menangani exit code dan error

Perintah yang gagal tidak otomatis melempar exception, kecuali kamu set check=True. Tanpa check, returncode yang bukan nol cuma terdiam, dan skripmu bisa lanjut dengan data yang salah. Ini sumber bug yang paling sering saya temukan di kode orang lain.

python
import subprocess

try:
    subprocess.run(["git", "push", "origin", "main"], check=True)
except subprocess.CalledProcessError as e:
    print(f"Push gagal, exit code {e.returncode}")
    print(e.stderr)

check=True membuat run() melempar CalledProcessError ketika returncode bukan nol, jadi skripmu berhenti di tempat yang benar. Kalau kamu belum nyaman dengan struktur try/except, saya sudah membahasnya tuntas di artikel Menguasai Exception Handling di Python.

Melewatkan input ke perintah

Sebagian program membaca input dari stdin — grep, sort, atau skrip interaktif. Parameter input= memungkinkan kamu mengirim string langsung tanpa perlu pipe di shell.

python
import subprocess

hasil = subprocess.run(
    ["grep", "-i", "error"],
    input="INFO server jalan\nERROR koneksi gagal\nDEBUG request masuk\n",
    capture_output=True,
    text=True,
)

print(hasil.stdout)  # ERROR koneksi gagal

Pipeline dan kapan shell=True diperlukan

Kadang kamu butuh fitur shell seperti pipe (|), wildcard (*), atau variabel $HOME. Itu gunanya shell=True. Tapi fitur ini membuka celah keamanan kalau ada bagian input yang datang dari pengguna.

python
import subprocess

# BERBAHAYA: jangan pakai input pengguna di sini
subprocess.run("ls *.txt | wc -l", shell=True)

Jangan pernah memakai shell=True dengan input yang bisa dikontrol pengguna. Seorang penyerang bisa menyisipkan perintah tambahan lewat titik koma — misalnya nama file "foto.jpg; rm -rf /" — dan server kamu bisa dibersihkan. Untuk input dinamis, selalu pakai daftar argumen.

Kalau kebutuhanmu cuma wildcard, perluas dulu polanya dengan modul glob, lalu teruskan hasilnya sebagai list. Tidak perlu shell sama sekali.

python
import glob
import subprocess

# Wildcard tanpa shell: perluas dulu pakai glob
subprocess.run(["wc", "-l", *glob.glob("*.log")])

Sebagian besar skrip yang saya lihat memakai shell=True sebenarnya tidak butuh. Kalau kamu lebih nyaman menulis perintah di shell daripada di Python, dasar-dasar yang sama juga saya bahas di Menguasai Bash Scripting di Linux.

Menjalankan proses lama dengan Popen

run() memblokir sampai perintah selesai. Untuk proses panjang yang mengeluarkan output bertahap — ping, build, tail -f — kamu butuh Popen supaya bisa membaca output baris per baris selagi proses masih berjalan.

python
import subprocess

proses = subprocess.Popen(
    ["ping", "-c", "4", "8.8.8.8"],
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
)

for baris in proses.stdout:
    print(baris, end="")

proses.wait()
print(f"Exit code: {proses.returncode}")

Popen juga cocok untuk streaming log server. stdout-nya kamu baca seperti file biasa, jadi bisa difilter atau ditulis ke berkas sambil proses jalan. Jangan lupa proses.wait() di akhir untuk memastikan proses benar-benar selesai sebelum skrip berhenti.

Contoh nyata: resize batch gambar

Sekarang rakit jadi satu skrip yang berguna. Skrip ini meresize semua file JPG di folder gambar_asli menjadi thumbnail 300x300 pakai ImageMagick, lalu menyimpan hasilnya di folder gambar_thumbnail. Di ImageMagick versi 7, ganti convert menjadi magick.

python
import subprocess
from pathlib import Path

SOURCE = Path("gambar_asli")
TARGET = Path("gambar_thumbnail")
TARGET.mkdir(exist_ok=True)

for foto in SOURCE.glob("*.jpg"):
    tujuan = TARGET / f"{foto.stem}_thumb.jpg"
    cmd = [
        "convert", str(foto),
        "-resize", "300x300",
        "-quality", "85",
        str(tujuan),
    ]
    hasil = subprocess.run(cmd, capture_output=True, text=True)
    if hasil.returncode == 0:
        print(f"OK    {foto.name}")
    else:
        print(f"GAGAL {foto.name}: {hasil.stderr.strip()}")

Belum punya ImageMagick? Install dulu dengan sudo apt install imagemagick di Debian/Ubuntu atau brew install imagemagick di macOS. Skrip ini jalan apa adanya selama perintah convert tersedia di PATH, dan setiap kegagalan dilaporkan per file tanpa menghentikan seluruh proses.

Untuk ribuan file, jalankan loop ini dengan ThreadPoolExecutor dari concurrent.futures supaya beberapa konversi berjalan paralel. Resize gambar relatif ringan di CPU, jadi 4-8 worker biasanya bikin prosesnya jauh lebih cepat.

Langkah berikutnya yang masuk akal: catat output tiap perintah ke file log supaya kamu tahu persis apa yang terjadi saat skrip jalan tanpa pengawasan, misalnya lewat cron. Modul logging standar Python cukup untuk ini, dan saya sudah menulis panduannya di Belajar Python Logging.