Skip to content

Python subprocess : exécuter des commandes externes depuis Python (guide complet)

Updated on

Les scripts Python doivent souvent appeler des programmes externes. Vous pouvez avoir besoin d’exécuter une commande shell pour compresser des fichiers, d’invoquer git pour le contrôle de version, d’appeler un utilitaire système comme ffmpeg pour traiter de la vidéo, ou d’exécuter un binaire compilé dans une chaîne de traitement de données. Mais s’appuyer sur os.system() ou des bidouilles façon backticks conduit à du code fragile, peu sûr, et impossible à déboguer quand les choses tournent mal.

Le problème s’aggrave vite. La sortie disparaît dans le néant parce que vous n’avez aucun moyen de la capturer. Les erreurs passent sous silence parce que le code de retour est ignoré. Un simple nom de fichier fourni par l’utilisateur contenant un espace ou un point-virgule peut transformer votre script innocent en vulnérabilité d’injection shell. Et quand un sous-processus se bloque, tout votre programme Python se bloque avec lui — pas de timeout, pas de récupération, pas d’explication.

Le module subprocess de Python est la solution standard. Il remplace des fonctions plus anciennes comme os.system(), os.popen() et le module commands (obsolète) par une API unique et cohérente pour lancer des processus, capturer leur sortie, gérer les erreurs, définir des timeouts et construire des pipelines. Ce guide couvre tout ce dont vous avez besoin pour l’utiliser efficacement et en toute sécurité.

📚

Démarrage rapide avec subprocess.run()

La fonction subprocess.run(), introduite en Python 3.5, est la manière recommandée d’exécuter des commandes externes. Elle lance une commande, attend sa fin, puis renvoie un objet CompletedProcess.

import subprocess
 
# Run a simple command
result = subprocess.run(["ls", "-la"], capture_output=True, text=True)
 
print(result.stdout)       # standard output as a string
print(result.stderr)       # standard error as a string
print(result.returncode)   # 0 means success

Paramètres clés :

  • capture_output=True capture stdout et stderr (équivalent à stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  • text=True décode la sortie en chaînes de caractères au lieu d’octets (bytes)
  • La commande est passée sous forme de liste de chaînes, où chaque argument est un élément distinct
import subprocess
 
# Run a command with arguments
result = subprocess.run(
    ["python", "--version"],
    capture_output=True,
    text=True
)
print(result.stdout.strip())  # e.g., "Python 3.12.1"

Comprendre la commande sous forme de liste vs chaîne

Le premier argument de subprocess.run() peut être une liste ou une chaîne. Cette distinction compte pour la justesse et la sécurité.

Forme liste (recommandée)

Chaque élément de la liste est un argument séparé. Python les transmet directement au système d’exploitation sans interprétation par un shell.

import subprocess
 
# Each argument is a separate list element
result = subprocess.run(
    ["grep", "-r", "TODO", "/home/user/project"],
    capture_output=True,
    text=True
)
print(result.stdout)

Les noms de fichiers contenant des espaces, guillemets ou caractères spéciaux fonctionnent correctement, car chaque argument est passé tel quel :

import subprocess
 
# Filename with spaces -- works correctly as a list element
result = subprocess.run(
    ["cat", "my file with spaces.txt"],
    capture_output=True,
    text=True
)

Forme chaîne (nécessite shell=True)

Passer une chaîne unique nécessite shell=True, ce qui invoque le shell système (/bin/sh sur Unix, cmd.exe sur Windows) pour interpréter la commande.

import subprocess
 
# String form requires shell=True
result = subprocess.run(
    "ls -la | grep '.py'",
    shell=True,
    capture_output=True,
    text=True
)
print(result.stdout)

Cela active des fonctionnalités du shell comme les pipes (|), les redirections (>), le globbing (*.py) et l’expansion de variables d’environnement ($HOME). Mais cela introduit aussi des risques de sécurité sérieux, que nous abordons dans la section Sécurité ci-dessous.

Capturer la sortie

Capturer stdout et stderr séparément

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

Fusionner stderr dans stdout

Parfois, vous voulez toute la sortie dans un seul flux. Utilisez stderr=subprocess.STDOUT :

import subprocess
 
result = subprocess.run(
    ["python", "-c", "import sys; print('out'); print('err', file=sys.stderr)"],
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True
)
 
print(result.stdout)  # Contains both "out\n" and "err\n"

Ignorer la sortie

Envoyez la sortie vers subprocess.DEVNULL pour la supprimer :

import subprocess
 
# Run silently -- discard all output
result = subprocess.run(
    ["apt-get", "update"],
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL
)

Sortie binaire

Omettez text=True pour recevoir des bytes bruts. Utile pour des données binaires comme des images ou des fichiers compressés :

import subprocess
 
# Capture binary output (e.g., from curl)
result = subprocess.run(
    ["curl", "-s", "https://example.com/image.png"],
    capture_output=True
)
 
image_bytes = result.stdout  # bytes object
print(f"Downloaded {len(image_bytes)} bytes")

Gestion des erreurs

Vérifier manuellement les codes de retour

Par défaut, subprocess.run() ne lève pas d’exception lorsqu’une commande échoue. Vous devez vérifier returncode vous-même :

import subprocess
 
result = subprocess.run(
    ["ls", "/nonexistent/path"],
    capture_output=True,
    text=True
)
 
if result.returncode != 0:
    print(f"Command failed with code {result.returncode}")
    print(f"Error: {result.stderr}")

Exception automatique en cas d’échec avec check=True

Le paramètre check=True lève subprocess.CalledProcessError quand le code de retour est non nul :

import subprocess
 
try:
    result = subprocess.run(
        ["ls", "/nonexistent/path"],
        capture_output=True,
        text=True,
        check=True
    )
except subprocess.CalledProcessError as e:
    print(f"Command failed with return code {e.returncode}")
    print(f"stderr: {e.stderr}")
    print(f"stdout: {e.stdout}")

C’est le pattern recommandé pour des commandes censées réussir à coup sûr. Cela vous force à gérer explicitement les échecs plutôt que de les ignorer silencieusement.

Gérer le cas « commande introuvable »

Si l’exécutable n’existe pas, Python lève FileNotFoundError :

import subprocess
 
try:
    result = subprocess.run(
        ["nonexistent_command"],
        capture_output=True,
        text=True
    )
except FileNotFoundError:
    print("Command not found -- is it installed and in PATH?")

Timeouts

Des processus longs ou bloqués peuvent figer votre script pour toujours. Le paramètre timeout (en secondes) tue le processus et lève subprocess.TimeoutExpired s’il ne se termine pas à temps :

import subprocess
 
try:
    result = subprocess.run(
        ["sleep", "30"],
        timeout=5,
        capture_output=True,
        text=True
    )
except subprocess.TimeoutExpired:
    print("Process timed out after 5 seconds")

C’est essentiel pour des commandes réseau, des API externes, ou tout processus susceptible de se bloquer :

import subprocess
 
def run_with_timeout(cmd, timeout_seconds=30):
    """Run a command with timeout and error handling."""
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout_seconds,
            check=True
        )
        return result.stdout
    except subprocess.TimeoutExpired:
        print(f"Command timed out after {timeout_seconds}s: {' '.join(cmd)}")
        return None
    except subprocess.CalledProcessError as e:
        print(f"Command failed (code {e.returncode}): {e.stderr}")
        return None
    except FileNotFoundError:
        print(f"Command not found: {cmd[0]}")
        return None
 
# Usage
output = run_with_timeout(["ping", "-c", "4", "example.com"], timeout_seconds=10)
if output:
    print(output)

Passer de l’entrée à un processus

Utilisez le paramètre input pour envoyer des données vers stdin du processus :

import subprocess
 
# Send text to stdin
result = subprocess.run(
    ["grep", "error"],
    input="line 1\nerror on line 2\nline 3\nerror on line 4\n",
    capture_output=True,
    text=True
)
 
print(result.stdout)
# "error on line 2\nerror on line 4\n"

Cela remplace le schéma courant consistant à « piper » des données via des commandes shell :

import subprocess
import json
 
# Send JSON to a processing command
data = {"name": "Alice", "score": 95}
json_string = json.dumps(data)
 
result = subprocess.run(
    ["python", "-c", "import sys, json; d = json.load(sys.stdin); print(d['name'])"],
    input=json_string,
    capture_output=True,
    text=True
)
 
print(result.stdout.strip())  # "Alice"

Variables d’environnement

Par défaut, les sous-processus héritent de l’environnement courant. Vous pouvez le modifier :

import subprocess
import os
 
# Add or override environment variables
custom_env = os.environ.copy()
custom_env["API_KEY"] = "secret123"
custom_env["DEBUG"] = "true"
 
result = subprocess.run(
    ["python", "-c", "import os; print(os.environ.get('API_KEY'))"],
    env=custom_env,
    capture_output=True,
    text=True
)
 
print(result.stdout.strip())  # "secret123"

Utilisez toujours os.environ.copy() comme base. Passer un dict sans l’environnement existant supprime toutes les variables héritées, ce qui peut casser des commandes qui dépendent de PATH, HOME ou d’autres variables système.

Répertoire de travail

Le paramètre cwd définit le répertoire de travail du sous-processus :

import subprocess
 
# Run git status in a specific repository
result = subprocess.run(
    ["git", "status", "--short"],
    cwd="/home/user/my-project",
    capture_output=True,
    text=True
)
 
print(result.stdout)

subprocess.run() vs Popen : quand utiliser quoi

subprocess.run() est un wrapper pratique autour de subprocess.Popen. Pour la plupart des cas d’usage, run() suffit. Utilisez Popen quand vous avez besoin de :

  • Streaming de sortie en temps réel (ligne par ligne au fur et à mesure)
  • Interagir avec un processus en cours (envoyer de l’entrée, lire la sortie dans une boucle)
  • Construire des pipelines multi-étapes reliant plusieurs processus
  • Exécution non bloquante avec contrôle manuel du cycle de vie du processus

Tableau comparatif

Fonctionnalitésubprocess.run()subprocess.Popenos.system()
RecommandéOui (Python 3.5+)Oui (avancé)Non (pattern obsolète)
Capturer la sortieOui (capture_output=True)Oui (via PIPE)Non
Valeur de retourObjet CompletedProcessObjet processus PopenCode de sortie (int)
Support du timeoutOui (timeout param)Manuel (via wait/communicate)Non
Vérification d’erreurcheck=True lève une exceptionManuelDoit analyser le code de sortie
Entrée vers stdinParamètre inputcommunicate() ou stdin.write()Non
Sortie en temps réelNon (attend la fin)Oui (flux ligne par ligne)La sortie va au terminal
PipelinesLimité (commande unique)Oui (enchaîner plusieurs Popen)Oui (via chaîne shell)
SécuritéSûr avec args en listeSûr avec args en listeRisque d’injection shell
Fonctionnalités shellSeulement avec shell=TrueSeulement avec shell=TrueUtilise toujours le shell

Avancé : subprocess.Popen

Popen vous donne un contrôle total sur le cycle de vie du processus. Le constructeur démarre le processus immédiatement et renvoie un objet Popen avec lequel vous pouvez interagir.

Utilisation de base de Popen

import subprocess
 
proc = subprocess.Popen(
    ["ls", "-la"],
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)
 
stdout, stderr = proc.communicate()  # Wait for completion and get output
print(f"Return code: {proc.returncode}")
print(stdout)

Streamer la sortie en temps réel

Contrairement à run(), Popen vous permet de lire la sortie ligne par ligne au fur et à mesure :

import subprocess
 
proc = subprocess.Popen(
    ["ping", "-c", "5", "example.com"],
    stdout=subprocess.PIPE,
    text=True
)
 
# Read output line by line as it arrives
for line in proc.stdout:
    print(f"[LIVE] {line.strip()}")
 
proc.wait()  # Wait for process to finish
print(f"Exit code: {proc.returncode}")

C’est indispensable pour des commandes longues, lorsque vous voulez afficher la progression ou journaliser la sortie en temps réel :

import subprocess
import sys
 
def run_with_live_output(cmd):
    """Run a command and stream its output in real time."""
    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        bufsize=1  # Line-buffered
    )
 
    output_lines = []
    for line in proc.stdout:
        line = line.rstrip()
        print(line)
        output_lines.append(line)
 
    proc.wait()
    return proc.returncode, "\n".join(output_lines)
 
# Usage
code, output = run_with_live_output(["pip", "install", "requests"])
print(f"\nFinished with exit code: {code}")

Construire des pipelines

Connectez plusieurs commandes en redirigeant stdout d’un processus vers stdin d’un autre :

import subprocess
 
# Equivalent to: cat /var/log/syslog | grep "error" | wc -l
p1 = subprocess.Popen(
    ["cat", "/var/log/syslog"],
    stdout=subprocess.PIPE
)
 
p2 = subprocess.Popen(
    ["grep", "error"],
    stdin=p1.stdout,
    stdout=subprocess.PIPE
)
 
# Allow p1 to receive SIGPIPE if p2 exits early
p1.stdout.close()
 
p3 = subprocess.Popen(
    ["wc", "-l"],
    stdin=p2.stdout,
    stdout=subprocess.PIPE,
    text=True
)
 
p2.stdout.close()
 
output, _ = p3.communicate()
print(f"Error count: {output.strip()}")

L’appel p1.stdout.close() après la connexion à p2 est important. Il permet à p1 de recevoir un signal SIGPIPE si p2 se termine plus tôt, ce qui évite des deadlocks.

Communication interactive avec un processus

import subprocess
 
# Start a Python REPL as a subprocess
proc = subprocess.Popen(
    ["python", "-i"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)
 
# Send commands and get results
stdout, stderr = proc.communicate(input="print(2 + 2)\nprint('hello')\n")
print(f"stdout: {stdout}")
print(f"stderr: {stderr}")

shell=True : puissance et danger

Définir shell=True exécute la commande via le shell système, ce qui active des fonctionnalités de shell mais introduit des risques de sécurité.

Quand shell=True est utile

import subprocess
 
# Shell features: pipes, redirects, globbing, env vars
result = subprocess.run(
    "ls *.py | wc -l",
    shell=True,
    capture_output=True,
    text=True
)
print(f"Python files: {result.stdout.strip()}")
 
# Environment variable expansion
result = subprocess.run(
    "echo $HOME",
    shell=True,
    capture_output=True,
    text=True
)
print(result.stdout.strip())

Le problème d’injection shell

Ne passez jamais une entrée utilisateur non assainie à shell=True :

import subprocess
 
# DANGEROUS -- shell injection vulnerability
user_input = "file.txt; rm -rf /"  # malicious input
subprocess.run(f"cat {user_input}", shell=True)  # Executes "rm -rf /"!
 
# SAFE -- use list form without shell=True
subprocess.run(["cat", user_input])  # Treats entire string as filename

Si vous devez utiliser shell=True avec des valeurs dynamiques, utilisez shlex.quote() :

import subprocess
import shlex
 
user_input = "file with spaces.txt; rm -rf /"
safe_input = shlex.quote(user_input)
 
# shlex.quote wraps in single quotes, neutralizing shell metacharacters
result = subprocess.run(
    f"cat {safe_input}",
    shell=True,
    capture_output=True,
    text=True
)

Mais l’approche la plus sûre est d’éviter complètement shell=True et de reproduire les fonctionnalités du shell en Python :

import subprocess
import glob
 
# Instead of: subprocess.run("ls *.py | wc -l", shell=True)
py_files = glob.glob("*.py")
print(f"Python files: {len(py_files)}")
 
# Instead of: subprocess.run("cat file1.txt file2.txt > combined.txt", shell=True)
with open("combined.txt", "w") as outfile:
    result = subprocess.run(
        ["cat", "file1.txt", "file2.txt"],
        stdout=outfile
    )

Bonnes pratiques de sécurité

PratiqueÀ faireÀ éviter
Format de commande["cmd", "arg1", "arg2"]f"cmd {user_input}" avec shell=True
Entrée utilisateurshlex.quote() si le shell est nécessaireConcaténation de chaînes dans les commandes
Mode shellshell=False (par défaut)shell=True avec une entrée non fiable
Chemin de l’exécutableChemins complets comme /usr/bin/gitSe reposer sur PATH pour du code critique
Validation des entréesValider et assainir avant de passerPasser l’entrée brute aux commandes
import subprocess
import shlex
from pathlib import Path
 
def safe_file_operation(filename):
    """Safely run a command with user-supplied filename."""
    # Validate input
    path = Path(filename)
    if not path.exists():
        raise FileNotFoundError(f"File not found: {filename}")
 
    # Check for path traversal
    resolved = path.resolve()
    allowed_dir = Path("/home/user/uploads").resolve()
    if not str(resolved).startswith(str(allowed_dir)):
        raise PermissionError("Access denied: file outside allowed directory")
 
    # Use list form -- no shell injection possible
    result = subprocess.run(
        ["wc", "-l", str(resolved)],
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout.strip()

Exemples concrets

Exécuter des commandes git

import subprocess
 
def git_status(repo_path):
    """Get git status for a repository."""
    result = subprocess.run(
        ["git", "status", "--porcelain"],
        cwd=repo_path,
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout.strip()
 
def git_log(repo_path, n=5):
    """Get last n commit messages."""
    result = subprocess.run(
        ["git", "log", f"--oneline", f"-{n}"],
        cwd=repo_path,
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout.strip()
 
status = git_status("/home/user/my-project")
if status:
    print("Uncommitted changes:")
    print(status)
else:
    print("Working directory clean")

Compresser et extraire des fichiers

import subprocess
 
def compress_directory(source_dir, output_file):
    """Create a tar.gz archive of a directory."""
    subprocess.run(
        ["tar", "-czf", output_file, "-C", source_dir, "."],
        check=True
    )
    print(f"Created archive: {output_file}")
 
def extract_archive(archive_file, dest_dir):
    """Extract a tar.gz archive."""
    subprocess.run(
        ["tar", "-xzf", archive_file, "-C", dest_dir],
        check=True
    )
    print(f"Extracted to: {dest_dir}")
 
compress_directory("/home/user/data", "/tmp/data_backup.tar.gz")

Vérifier des informations système

import subprocess
 
def get_disk_usage():
    """Get disk usage summary."""
    result = subprocess.run(
        ["df", "-h", "/"],
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout
 
def get_memory_info():
    """Get memory usage on Linux."""
    result = subprocess.run(
        ["free", "-h"],
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout
 
def get_process_list(filter_name=None):
    """List running processes, optionally filtered."""
    cmd = ["ps", "aux"]
    result = subprocess.run(cmd, capture_output=True, text=True, check=True)
 
    if filter_name:
        lines = result.stdout.strip().split("\n")
        header = lines[0]
        matching = [line for line in lines[1:] if filter_name in line]
        return header + "\n" + "\n".join(matching)
 
    return result.stdout
 
print(get_disk_usage())

Traiter des fichiers de données avec des outils externes

import subprocess
import csv
import io
 
def sort_csv_by_column(input_file, column_index=1):
    """Sort a CSV file using the system sort command (fast for large files)."""
    result = subprocess.run(
        ["sort", "-t,", f"-k{column_index}", input_file],
        capture_output=True,
        text=True,
        check=True
    )
    return result.stdout
 
def count_lines(filepath):
    """Count lines in a file using wc (faster than Python for huge files)."""
    result = subprocess.run(
        ["wc", "-l", filepath],
        capture_output=True,
        text=True,
        check=True
    )
    return int(result.stdout.strip().split()[0])
 
def search_in_files(directory, pattern, file_type="*.py"):
    """Search for a pattern in files using grep."""
    result = subprocess.run(
        ["grep", "-rn", "--include", file_type, pattern, directory],
        capture_output=True,
        text=True
    )
    # grep returns exit code 1 if no matches found (not an error)
    if result.returncode == 0:
        return result.stdout
    elif result.returncode == 1:
        return ""  # No matches
    else:
        raise subprocess.CalledProcessError(result.returncode, result.args)
 
matches = search_in_files("/home/user/project", "TODO")
if matches:
    print(matches)
else:
    print("No TODOs found")

Script de déploiement automatisé

import subprocess
import sys
 
def deploy(repo_path, branch="main"):
    """Simple deployment script using subprocess."""
    steps = [
        (["git", "fetch", "origin"], "Fetching latest changes"),
        (["git", "checkout", branch], f"Switching to {branch}"),
        (["git", "pull", "origin", branch], "Pulling latest code"),
        (["pip", "install", "-r", "requirements.txt"], "Installing dependencies"),
        (["python", "manage.py", "migrate"], "Running migrations"),
        (["python", "manage.py", "collectstatic", "--noinput"], "Collecting static files"),
    ]
 
    for cmd, description in steps:
        print(f"\n--- {description} ---")
        try:
            result = subprocess.run(
                cmd,
                cwd=repo_path,
                capture_output=True,
                text=True,
                check=True,
                timeout=120
            )
            if result.stdout:
                print(result.stdout)
        except subprocess.CalledProcessError as e:
            print(f"FAILED: {e.stderr}")
            sys.exit(1)
        except subprocess.TimeoutExpired:
            print(f"TIMEOUT: {description} took too long")
            sys.exit(1)
 
    print("\nDeployment complete")

Considérations multiplateformes

Les commandes se comportent différemment sur Windows et sur Unix. Écrivez du code portable :

import subprocess
import platform
 
def run_command(cmd_unix, cmd_windows=None):
    """Run a command with platform awareness."""
    if platform.system() == "Windows":
        cmd = cmd_windows or cmd_unix
        # Windows often needs shell=True for built-in commands
        return subprocess.run(cmd, shell=True, capture_output=True, text=True)
    else:
        return subprocess.run(cmd, capture_output=True, text=True)
 
# List directory contents
result = run_command(
    cmd_unix=["ls", "-la"],
    cmd_windows="dir"
)
print(result.stdout)

Principales différences selon la plateforme :

FonctionnalitéUnix/macOSWindows
Shell/bin/shcmd.exe
Séparateur de chemin/\\
Commandes intégrées (dir, copy)Non disponiblesNécessitent shell=True
Extension des exécutablesNon nécessaire.exe parfois nécessaire
Gestion des signauxSignaux POSIX completsLimitée
shlex.quote()FonctionneUtiliser subprocess.list2cmdline()

Exécuter subprocess dans des notebooks Jupyter

Exécuter des commandes shell depuis des notebooks Jupyter est un workflow courant pour les data scientists. Même si Jupyter supporte la syntaxe !command pour des appels rapides au shell, subprocess vous donne une gestion d’erreur correcte et la capture de sortie dans votre code Python.

Lors du débogage d’appels subprocess dans des notebooks — surtout quand des commandes échouent silencieusement ou produisent une sortie inattendue — RunCell (opens in a new tab) peut aider. RunCell est un agent IA pour Jupyter qui comprend le contexte de votre notebook. Il peut diagnostiquer pourquoi une commande subprocess échoue, suggérer les bons arguments et gérer les particularités selon la plateforme. Au lieu d’alterner entre terminal et notebook pour déboguer une commande shell, RunCell trace le problème directement dans votre cellule.

import subprocess
 
# In a Jupyter notebook: capture and display command output
result = subprocess.run(
    ["pip", "list", "--format=columns"],
    capture_output=True,
    text=True
)
 
# Display as formatted output in the notebook
print(result.stdout)

Erreurs courantes et comment les corriger

Erreur 1 : oublier de capturer la sortie

import subprocess
 
# Output goes to terminal, not captured
result = subprocess.run(["ls", "-la"])
print(result.stdout)  # None!
 
# Fix: add capture_output=True
result = subprocess.run(["ls", "-la"], capture_output=True, text=True)
print(result.stdout)  # Actual output

Erreur 2 : utiliser une chaîne sans shell=True

import subprocess
 
# Fails: string passed without shell=True
# subprocess.run("ls -la")  # FileNotFoundError: "ls -la" is not a program
 
# Fix option 1: use a list
subprocess.run(["ls", "-la"])
 
# Fix option 2: use shell=True (less safe)
subprocess.run("ls -la", shell=True)

Erreur 3 : ignorer les erreurs

import subprocess
 
# Bad: silently continues on failure
result = subprocess.run(["rm", "/important/file"], capture_output=True, text=True)
# ... continues even if rm failed
 
# Good: check=True raises exception on failure
try:
    result = subprocess.run(
        ["rm", "/important/file"],
        capture_output=True,
        text=True,
        check=True
    )
except subprocess.CalledProcessError as e:
    print(f"Failed to delete: {e.stderr}")

Erreur 4 : deadlock avec Popen

import subprocess
 
# DEADLOCK: stdout buffer fills up, process blocks, .wait() waits forever
proc = subprocess.Popen(["command_with_lots_of_output"], stdout=subprocess.PIPE)
proc.wait()  # Deadlock!
 
# Fix: use communicate() which handles buffering
proc = subprocess.Popen(["command_with_lots_of_output"], stdout=subprocess.PIPE, text=True)
stdout, stderr = proc.communicate()  # Safe

Erreur 5 : ne pas gérer l’encodage

import subprocess
 
# Bytes output can cause issues
result = subprocess.run(["cat", "data.txt"], capture_output=True)
# result.stdout is bytes, not str
 
# Fix: use text=True or encoding parameter
result = subprocess.run(["cat", "data.txt"], capture_output=True, text=True)
 
# For specific encodings:
result = subprocess.run(
    ["cat", "data.txt"],
    capture_output=True,
    encoding="utf-8",
    errors="replace"  # Handle invalid bytes
)

Référence complète des paramètres de subprocess.run()

import subprocess
 
result = subprocess.run(
    args,                    # Command as list or string
    stdin=None,              # Input source (PIPE, DEVNULL, file object, or None)
    stdout=None,             # Output destination
    stderr=None,             # Error destination
    capture_output=False,    # Shorthand for stdout=PIPE, stderr=PIPE
    text=False,              # Decode output as strings (alias: universal_newlines)
    shell=False,             # Run through system shell
    cwd=None,                # Working directory
    timeout=None,            # Seconds before TimeoutExpired
    check=False,             # Raise CalledProcessError on non-zero exit
    env=None,                # Environment variables dict
    encoding=None,           # Output encoding (alternative to text=True)
    errors=None,             # Encoding error handling ('strict', 'replace', 'ignore')
    input=None,              # String/bytes to send to stdin
)

FAQ

Qu’est-ce que le module subprocess en Python ?

Le module subprocess est la bibliothèque standard de Python pour exécuter des commandes et des programmes externes depuis des scripts Python. Il a remplacé des approches plus anciennes comme os.system(), os.popen() et le module commands. Il fournit des fonctions pour lancer de nouveaux processus, se connecter à leurs pipes d’entrée/sortie/erreur, récupérer les codes de retour et gérer les timeouts. L’interface principale est subprocess.run() pour exécuter simplement des commandes, et subprocess.Popen pour des cas avancés nécessitant des E/S en temps réel ou des pipelines de processus.

Quelle est la différence entre subprocess.run() et subprocess.Popen ?

subprocess.run() est une fonction de haut niveau (pratique) qui exécute une commande, attend sa fin et renvoie un objet CompletedProcess contenant la sortie. C’est le bon choix pour la plupart des tâches. subprocess.Popen est une classe de plus bas niveau qui vous donne le contrôle direct du processus : vous pouvez streamer la sortie ligne par ligne, envoyer de l’entrée de manière interactive, construire des pipelines multi-processus et gérer manuellement le cycle de vie du processus. Utilisez Popen quand vous avez besoin de streaming en temps réel ou de connecter plusieurs processus.

shell=True est-il dangereux dans subprocess ?

Oui, utiliser shell=True avec une entrée non fiable crée une vulnérabilité d’injection shell. Quand shell=True est activé, la chaîne de commande est transmise au shell système pour interprétation, ce qui signifie que des métacaractères comme ;, |, && et $() sont exécutés. Un attaquant peut injecter des commandes arbitraires. Le choix sûr par défaut est shell=False avec des commandes passées sous forme de liste. Si vous devez utiliser shell=True, assainissez l’entrée avec shlex.quote() et ne passez jamais une entrée utilisateur brute.

Comment capturer la sortie d’une commande subprocess ?

Utilisez capture_output=True et text=True avec subprocess.run(). La sortie est stockée dans result.stdout (en tant que chaîne) et les erreurs dans result.stderr. Par exemple : result = subprocess.run(["ls", "-la"], capture_output=True, text=True) puis accéder à result.stdout. Sans text=True, la sortie est renvoyée sous forme de bytes.

Comment gérer les timeouts subprocess en Python ?

Passez le paramètre timeout (en secondes) à subprocess.run(). Si le processus dépasse le timeout, Python le tue et lève subprocess.TimeoutExpired. Exemple : subprocess.run(["slow_command"], timeout=30). Pour Popen, utilisez proc.communicate(timeout=30) ou proc.wait(timeout=30). Encapsulez toujours le code sensible au timeout dans un bloc try/except.

Pourquoi os.system() fonctionne-t-il encore si subprocess est recommandé ?

os.system() n’est pas formellement obsolète, mais c’est une interface héritée. Elle exécute des commandes via le shell (comme shell=True), ne peut pas capturer la sortie, n’offre aucun mécanisme de timeout, et ne renvoie que le statut de sortie. subprocess.run() fait tout ce que os.system() fait, en plus de la capture de sortie, de la gestion d’erreurs, des timeouts et du passage sécurisé des arguments. Tout nouveau code devrait utiliser subprocess.

Conclusion

Le module subprocess est l’outil de référence de Python pour exécuter des commandes externes. Utilisez subprocess.run() pour l’exécution simple de commandes — il gère la capture de sortie, la vérification d’erreurs, les timeouts et le passage d’entrée en un seul appel. Ne passez à subprocess.Popen que lorsque vous avez besoin de streaming de sortie en temps réel, de communication interactive avec un processus, ou de pipelines multi-étapes.

L’habitude la plus importante est d’éviter shell=True avec une entrée fournie par l’utilisateur. Passez les commandes sous forme de listes pour éliminer totalement les risques d’injection shell. Utilisez check=True pour détecter les échecs tôt. Définissez un timeout pour éviter les processus bloqués. Et utilisez text=True pour manipuler des chaînes plutôt que des bytes bruts.

De l’automatisation git à l’orchestration de pipelines de données, subprocess vous donne le contrôle et la sécurité que os.system() n’a jamais pu offrir. Maîtrisez ces patterns et vous pourrez intégrer en confiance n’importe quel outil externe dans vos workflows Python.

📚