🐍 Python Course · Stage 3 · Lesson 42 of 89 · subprocess — External Programs
Course Overview →

🔵 Read Beginner and Intermediate tabs. The Expert tab has the internals — worth reading once you finish Stage 3.

← csv — CSV Files argparse — CLI Arguments →
thecodex.expert · The Codex Family of Knowledge
Python

subprocess — Running External Programs

The subprocess module runs external commands and programs from Python — capturing their output, sending input, checking exit codes, and managing pipelines safely.

Python 3.13 docs.python.org subprocess Last verified:
Canonical Definition

subprocess is a standard library module for spawning new processes, connecting to their input/output/error pipes, and obtaining their return codes. subprocess.run() is the recommended high-level API for most use cases. It replaces older functions like os.system() and os.popen().

🟩 Beginner

Running external programs from Python

What you'll learn: How to run external commands with subprocess.run(), check exit codes, and capture a command's output into your Python program.

How to read this tab: Try subprocess.run(["echo", "hello"]) first. Then add capture_output=True, text=True and inspect result.stdout. Build up from running a command to capturing its output.

⏱ 30 min📄 2 sections🔶 Prerequisite: Modules & Packages
The idea

subprocess lets your Python program run other programs — like running a terminal command from inside your code. Want to run git status, call ffmpeg, or execute a shell script? subprocess is how. subprocess.run() is the one function to learn first.

Pass arguments as a list, not a string. subprocess.run(["ls", "-l", "/home"]) — each argument is a separate list item. subprocess.run("ls -l /home") looks for a program literally named "ls -l /home" and fails. The list form is also injection-safe. This is the most common subprocess mistake.

subprocess.run() — the main API

The subprocess.run() function (Python 3.5+) is the recommended way to run external commands. Pass the command as a list of strings — the program name first, then each argument as a separate list item.

Pythonsubprocess_basics.py
import subprocess

# Run a command — arguments as a list
result = subprocess.run(["echo", "Hello from subprocess"])
# Hello from subprocess  (printed to terminal)

# Check the exit code
result = subprocess.run(["ls", "/tmp"])
print(result.returncode)   # 0 means success, non-zero means error

# Run git status (in a git repository)
result = subprocess.run(["git", "status"])

# IMPORTANT: pass arguments as separate list items, NOT one string
# RIGHT:
subprocess.run(["ls", "-l", "/home"])
# WRONG (unless shell=True):
# subprocess.run("ls -l /home")   <- this looks for a program
#                                    literally named "ls -l /home"

# Raise an exception if the command fails (check=True)
try:
    subprocess.run(["false"], check=True)   # 'false' exits with code 1
except subprocess.CalledProcessError as e:
    print(f"Command failed with code {e.returncode}")
💡

capture_output=True, text=True is the combination you want. capture_output captures stdout and stderr into the result object. text=True decodes them from bytes to strings (otherwise you get b"output"). Without these, output goes straight to the terminal and you can't process it in your program.

Capturing output

By default, a subprocess's output goes to the terminal. To capture it in your Python program, pass capture_output=True and text=True.

Pythonsubprocess_output.py
import subprocess

# Capture stdout as text
result = subprocess.run(
    ["git", "branch", "--show-current"],
    capture_output=True,   # capture stdout and stderr
    text=True,             # decode bytes to str (instead of raw bytes)
)
print(f"Current branch: {result.stdout.strip()}")

# Capture and process output
result = subprocess.run(
    ["ls", "-1", "/usr/bin"],
    capture_output=True,
    text=True,
)
programs = result.stdout.splitlines()
print(f"Found {len(programs)} programs in /usr/bin")

# stdout and stderr are separate
result = subprocess.run(
    ["python3", "-c", "import sys; print('out'); print('err', file=sys.stderr)"],
    capture_output=True,
    text=True,
)
print(f"stdout: {result.stdout}")   # out
print(f"stderr: {result.stderr}")   # err

# Get output as a simple string (convenience for capture + check)
output = subprocess.check_output(["date"], text=True)
print(output.strip())

✅ Beginner tab complete

  • I can run a command with subprocess.run(["program", "arg1", "arg2"])
  • I pass arguments as separate list items, NOT as one string
  • I can capture output with capture_output=True, text=True
  • I can check success with result.returncode or check=True

Stage 3 stdlib expansion complete for this batch. Continue the Python Course →

🔵 Intermediate

Input, pipelines, timeouts, and security

What you'll learn: Sending input to a process, building pipelines, setting timeouts and working directories, and the critical security rule about shell=True and command injection.

How to read this tab: Read the security section carefully. The difference between the list form and shell=True is the difference between safe and exploitable code. This is one of the most important security lessons in Python.

⏱ 30 min📄 2 sections🔶 Prerequisite: Beginner tab
💡

Set a timeout to avoid hung processes. subprocess.run(["command"], timeout=30) raises TimeoutExpired if the command runs longer than 30 seconds. Without a timeout, a hung external program hangs your entire Python program indefinitely. For any command that talks to a network or external system, set a timeout.

Sending input and building pipelines

You can send data to a program's standard input via the input parameter, set a timeout, and even chain programs together as a pipeline.

Pythonsubprocess_input.py
import subprocess

# Send input to a program's stdin
result = subprocess.run(
    ["grep", "error"],
    input="line 1\nerror line\nline 3\n",   # fed to stdin
    capture_output=True,
    text=True,
)
print(result.stdout)   # error line

# Set a timeout — kill the process if it runs too long
try:
    subprocess.run(["sleep", "10"], timeout=2)
except subprocess.TimeoutExpired:
    print("Command timed out after 2 seconds")

# Set the working directory and environment
result = subprocess.run(
    ["pwd"],
    cwd="/tmp",                          # run in this directory
    capture_output=True, text=True,
)
print(result.stdout.strip())   # /tmp

import os
custom_env = {**os.environ, "MY_VAR": "value"}
subprocess.run(["printenv", "MY_VAR"], env=custom_env)

# Pipelines — connect one program's output to another's input
# Equivalent to: ls /usr/bin | grep python
ls = subprocess.run(["ls", "/usr/bin"], capture_output=True, text=True)
grep = subprocess.run(
    ["grep", "python"],
    input=ls.stdout,         # feed ls output into grep
    capture_output=True, text=True,
)
print(grep.stdout)

# For true streaming pipelines, use Popen
p1 = subprocess.Popen(["ls", "/usr/bin"], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["grep", "python"], stdin=p1.stdout,
                      stdout=subprocess.PIPE, text=True)
p1.stdout.close()   # allow p1 to receive SIGPIPE if p2 exits
output, _ = p2.communicate()
print(output)

shell=True with user input is a command injection vulnerability. subprocess.run(f"cat {filename}", shell=True) where filename is user-controlled lets an attacker run x.txt; rm -rf /. The list form subprocess.run(["cat", filename]) treats the entire filename as a single argument — injection-proof. Never use shell=True with any input you don't fully control.

Security: shell=True and command injection

The most important subprocess safety rule: avoid shell=True with untrusted input. It opens your program to command injection attacks.

Pythonsubprocess_security.py
import subprocess

# DANGEROUS — command injection vulnerability
filename = input("Enter filename: ")   # user types: x.txt; rm -rf /
subprocess.run(f"cat {filename}", shell=True)   # NEVER DO THIS
# The shell interprets ; rm -rf / as a second command!

# SAFE — arguments as a list, no shell
filename = input("Enter filename: ")
subprocess.run(["cat", filename])   # filename is a single argument
# Even "x.txt; rm -rf /" is treated as one (invalid) filename — safe

# If you MUST use shell features (pipes, globbing, redirection in the
# shell), sanitise input with shlex.quote — but prefer the list form:
import shlex
safe_arg = shlex.quote(filename)

# The list form also handles spaces and special characters correctly:
subprocess.run(["cat", "file with spaces.txt"])   # works perfectly
# vs shell=True where you'd need careful quoting
Commonly confused
text=True vs the default bytes. Without text=True, stdout and stderr are bytes objects (b"output"), not strings. Pass text=True (or its alias universal_newlines=True) to get decoded strings. This also enables universal newline handling.
Use run(), not the old os.system(). os.system() runs through the shell (injection risk), can't capture output cleanly, and only returns the exit code. subprocess.run() is safer, more capable, and the officially recommended replacement. Also avoid the older subprocess.call(), check_call() — run() does it all.

✅ Intermediate tab complete

  • I can send input to a process with the input= parameter
  • I can set a timeout= to kill a hung process
  • I NEVER use shell=True with untrusted input (command injection risk)
  • I use the list form ["cmd", arg] which is injection-safe by default

Continue the Python Course →

🔴 Expert

Popen, the process lifecycle, and async subprocess

What you'll learn: The Popen class for fine-grained process control, streaming output as it's produced, poll() vs wait(), communicate() for deadlock-free pipe handling, and asyncio.create_subprocess_exec for concurrent processes.

How to read this tab: Use Popen to stream the output of a long-running command (like ping) line by line as it's produced, rather than waiting for it to finish.

⏱ 25 min📄 1 section🔶 Prerequisite: After Stage 2

Popen, process lifecycle, and async subprocess

Under the hood, subprocess.run() is a convenience wrapper around the Popen class, which provides fine-grained control over process creation and the process lifecycle. Use Popen directly when you need to interact with a long-running process, stream output as it's produced, or manage multiple processes concurrently.

Pythonsubprocess_popen.py
import subprocess

# Popen — start a process and interact with it while it runs
proc = subprocess.Popen(
    ["ping", "-c", "5", "example.com"],
    stdout=subprocess.PIPE,
    text=True,
)

# Stream output line by line as the process produces it
for line in proc.stdout:
    print(f"Got: {line.strip()}")

proc.wait()   # wait for the process to finish
print(f"Exit code: {proc.returncode}")

# poll() — check if the process is done without blocking
import time
proc = subprocess.Popen(["sleep", "3"])
while proc.poll() is None:    # None means still running
    print("Still running...")
    time.sleep(1)
print("Done")

# Async subprocess with asyncio
import asyncio

async def run_command(cmd):
    proc = await asyncio.create_subprocess_exec(
        *cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout, stderr = await proc.communicate()
    return stdout.decode()

async def main():
    # Run multiple commands concurrently
    results = await asyncio.gather(
        run_command(["echo", "first"]),
        run_command(["echo", "second"]),
        run_command(["echo", "third"]),
    )
    print(results)

asyncio.run(main())

The process creation mechanism differs by platform: on POSIX systems, subprocess uses fork() followed by exec() (or the more efficient posix_spawn() where available); on Windows it uses CreateProcess(). The communicate() method is the safe way to interact with a process's pipes — it reads stdout and stderr concurrently, avoiding the deadlock that occurs if you naively read one pipe while the process blocks writing to the other.

✅ Expert tab complete

  • I can use Popen to start a process and stream its output while it runs
  • I know communicate() reads stdout and stderr concurrently to avoid deadlock
  • I can run subprocesses concurrently with asyncio.create_subprocess_exec

Continue the Python Course →

Sources

1
Python Standard Library — subprocess. docs.python.org/3/library/subprocess.html.
2
PEP 324 — subprocess - New process module. peps.python.org/pep-0324/.
3
Python Standard Library — subprocess.run and subprocess.Popen. docs.python.org/3/library/subprocess.html#subprocess.run.
4
Python Standard Library — Security Considerations. docs.python.org/3/library/subprocess.html#security-considerations.
Source confidence: High Last verified: Primary source: docs.python.org subprocess