PythonMastery

How do I run a shell command from Python?

subprocess.run(['git', 'status'], capture_output=True, text=True, check=True). Pass the command as a list, and never use shell=True with anything a user typed.

subprocess.run with the command as a list: the program first, then each argument as its own item.

python · run on your machine
import subprocess

result = subprocess.run(
    ["git", "log", "--oneline", "-3"],
    capture_output=True,   # keep the output instead of printing it
    text=True,             # give me str, not bytes
    check=True,            # raise if the command fails
)
print(result.stdout)

This runs on your own machine only; a browser tab has no shell to run commands in. If commands and shells are new to you, the terminal lesson explains them first.

What each keyword buys you:

Handling a failure

python · run on your machine
import subprocess

try:
    subprocess.run(["git", "push"], capture_output=True, text=True, check=True, timeout=60)
except subprocess.CalledProcessError as e:
    print("git push failed with code", e.returncode)
    print(e.stderr)
except FileNotFoundError:
    print("git isn't installed, or isn't on PATH")

timeout=60 stops a command that hangs waiting for a password prompt you'll never see.

Why a list, and not one string with shell=True

With shell=True, the string goes to the shell, and the shell treats ;, && and | as instructions. If any part of the string came from a user, they can add their own command:

python · run on your machine
import subprocess

filename = input("File to count lines in: ")      # someone types:  notes.txt; rm -rf ~

# DANGEROUS: the shell runs both commands
subprocess.run(f"wc -l {filename}", shell=True)

# SAFE: the whole input is one argument, a strange file name and nothing more
subprocess.run(["wc", "-l", filename])

This is command injection, and the list form prevents it completely. If you're tempted by shell=True for a pipe or a wildcard, do that part in Python instead: pathlib.Path.glob() for wildcards, and read result.stdout rather than piping to grep.

You'll see os.system() in old answers. It always goes through the shell, can't capture output, and only tells you the exit code. subprocess.run replaced it.

Go deeper: Task Automation: Scripting the Boring Parts, Web Security Checklist, The Terminal for Python Developers

More short answers

all questions