Python subprocess.run - Appeler des commandes externes proprement

Xavier Moreau .

23 février 2026

Diagramme montrant l'interface en ligne de commande, le shell et le REPL Python, avec les flux stdin, stdout et stderr vers un clavier et une fenêtre utilisateur.

Quand un script Python doit lancer `git`, `npm`, `ffmpeg` ou un utilitaire interne, tout se joue dans la manière d’appeler le processus externe. La méthode `subprocess.run()` couvre l’essentiel des besoins courants: exécuter une commande, récupérer sa sortie, vérifier le code de retour et poser une limite de temps. Je vais aller droit au point utile: comment l’utiliser proprement, quand éviter le shell et où se cachent les pièges de sécurité et de portabilité.

Les points à retenir avant de lancer une commande externe

  • La documentation Python actuelle recommande `run()` pour les cas simples, et réserve `Popen` aux usages plus avancés.
  • Passer les arguments sous forme de liste est généralement le choix le plus sûr et le plus prévisible.
  • `capture_output=True` récupère `stdout` et `stderr`, mais ne se combine pas avec `stdout=` ou `stderr=`.
  • Avec `check=True`, un code de sortie non nul déclenche une exception au lieu de passer silencieusement.
  • `shell=True` n’est utile que si tu as réellement besoin des fonctions du shell, comme les pipes ou l’expansion des jokers.
  • Si la commande produit beaucoup de sortie ou doit rester interactive, il faut souvent passer à `Popen` ou à `asyncio`.

À quoi sert l’exécution de commandes externes en Python

Le module `subprocess` sert à créer un nouveau processus, lui transmettre des arguments, récupérer ses flux standards et lire son code de retour. La documentation Python actuelle recommande `run()` pour tous les cas qu’il peut couvrir, ce qui en fait le bon point de départ dans la majorité des scripts d’automatisation, des outils de déploiement et des tâches de build.

Concrètement, je l’utilise quand Python doit orchestrer un outil déjà existant plutôt que réimplémenter sa logique. Par exemple: lancer des tests avec `pytest`, vérifier l’état d’un dépôt Git, déclencher un build frontend, appeler un binaire de traitement d’images ou exécuter une commande d’administration sur un serveur. C’est typiquement le genre de besoin qu’on rencontre dans un backend, une chaîne CI/CD ou un script de maintenance.

Le vrai intérêt de cette approche, c’est le contrôle. Je peux choisir le répertoire de travail, l’environnement transmis au processus enfant, la manière de récupérer la sortie et le comportement en cas d’échec. C’est plus propre qu’un appel système brut, et bien plus lisible qu’une chaîne de shell bricolée à la main. Une fois ce cadre posé, le sujet devient surtout la bonne forme des arguments.

Passer les arguments correctement

Mon réflexe est simple: je passe presque toujours une liste d’arguments. C’est la forme la plus robuste, parce que Python gère lui-même l’échappement et le découpage des arguments. Une chaîne unique n’a de sens que dans des cas précis, notamment lorsque le shell est volontairement impliqué ou quand on ne passe qu’un nom de programme sans argument dans certains contextes POSIX.

Forme Quand l’utiliser Ce que ça apporte Ce qu’il faut surveiller
["git", "status", "--short"] Cas standard Arguments sûrs et prévisibles Rien de spécial, c’est la forme à privilégier
"git status --short" avec shell=True Quand le shell est indispensable Pipes, redirections, expansion de jokers Injection de commandes si la chaîne est mal construite
"git" seul Cas limité sur POSIX Appel minimal Peu utile dès qu’il faut des arguments

Quand j’ai une commande tapée par un humain dans un outil interne, j’évite de la transmettre telle quelle. Je la découpe avec `shlex.split()` pour obtenir une séquence correcte. Et si je dois relancer le Python courant, je passe plutôt `sys.executable` avec `-m`, par exemple pour appeler un module ou relancer un environnement virtuel sans ambiguïté.

import sys
from subprocess import run

run([sys.executable, "-m", "pip", "install", "requests"], check=True)
Le principe de fond est simple: je laisse Python faire le travail de séparation des arguments, au lieu de faire confiance à ma propre chaîne de caractères. C’est là que l’on évite la majorité des surprises, et cela prépare bien la récupération de sortie et la gestion d’erreurs.

Lire la sortie, vérifier le retour et gérer les erreurs

Quand `run()` termine, il renvoie un objet `CompletedProcess`. Tu y trouves les arguments réellement utilisés, le code de retour et, si tu as capturé les flux, le contenu de `stdout` et `stderr`. Par défaut, la sortie revient en bytes; si tu veux du texte, ajoute `text=True` ou un `encoding`, par exemple `utf-8`.

Je trouve que trois paramètres font la différence dans la vraie vie. D’abord `capture_output=True` pour récupérer `stdout` et `stderr` sans écrire le câblage à la main. Ensuite `check=True` pour faire échouer immédiatement un script si la commande externe retourne un code non nul. Enfin `timeout=` pour éviter qu’un processus bloqué ne fasse attendre tout le reste. Le délai est exprimé en secondes, ce qui suffit généralement pour les scripts d’automatisation.

from subprocess import run, CalledProcessError, TimeoutExpired

try:
    result = run(
        ["git", "status", "--short"],
        capture_output=True,
        text=True,
        check=True,
        timeout=5,
    )
    print(result.stdout)
except TimeoutExpired:
    print("La commande a dépassé le délai.")
except CalledProcessError as exc:
    print(f"Échec avec le code {exc.returncode}")
    print(exc.stderr)

Deux détails méritent d’être retenus. D’une part, `capture_output=True` ne peut pas être combiné avec `stdout=` ou `stderr=`. D’autre part, si tu veux fusionner sortie normale et sortie d’erreur, il faut rediriger `stderr` vers `STDOUT` plutôt que de compter sur `capture_output`. J’utilise aussi `DEVNULL` quand je n’ai pas besoin de la sortie et que je veux simplement éviter qu’elle encombre le script.

Et si la commande peut produire énormément de données, je me méfie: tout capturer en mémoire n’est pas toujours une bonne idée. C’est précisément là que l’on commence à sortir du cas simple, donc la question suivante devient celle de la sécurité et des limites de cette API.

Schéma montrant l'interface ligne de commande, le shell et le REPL Python, avec les flux stdin, stdout et stderr.

Sécurité et portabilité quand le shell entre en jeu

Je pars d’un principe strict: si je peux éviter `shell=True`, je l’évite. La documentation Python rappelle que le module ne lance pas implicitement un shell; cela permet justement de transmettre sans danger des caractères spéciaux à un programme enfant. Le risque apparaît dès qu’on force le passage par le shell, parce qu’alors le quoting et l’échappement deviennent ma responsabilité.

C’est pour cela que je réserve `shell=True` aux cas où le shell apporte une vraie valeur: pipelines, redirections, expansion de jokers ou commandes intégrées au shell. Sur Windows, cela a du sens surtout pour des commandes du shell comme `dir` ou `copy`. Pour un exécutable normal ou un fichier batch, ce n’est généralement pas nécessaire.

Si je n’ai pas le choix, je fais attention au quoting et je ne mélange jamais des données non fiables avec une chaîne shell construite à la volée. Sur certains systèmes, `shlex.quote()` aide à protéger les fragments à insérer, mais il ne dispense pas d’une conception prudente. La règle pratique est simple: la commande doit rester lisible, bornée et prévisible.

La portabilité demande aussi un peu de discipline. Un chemin absolu réduit les variations entre plateformes, et `shutil.which()` permet de résoudre proprement un binaire dans le `PATH`. De plus, `cwd` et `env` influencent le contexte d’exécution, donc je les fixe explicitement dès que le script dépend d’un répertoire précis ou d’une variable d’environnement particulière. C’est souvent ce petit effort qui évite les bugs “ça marche chez moi” au moment du déploiement.

Savoir quand quitter `run()` pour `Popen` ou `asyncio`

`run()` est excellent pour une commande simple, synchrone, courte et bornée. Dès que j’ai besoin de lire la sortie au fil de l’eau, d’échanger avec le processus, de faire tourner plusieurs commandes en parallèle ou de garder un contrôle fin sur le cycle de vie, je change d’outil. C’est moins une question de préférence qu’une question de forme du problème.

Besoins API adaptée Pourquoi
Une commande ponctuelle run() Simple, lisible, retour direct
Flux à lire progressivement Popen Contrôle fin sur `stdin`, `stdout` et `stderr`
Plusieurs processus en parallèle asyncio.create_subprocess_exec() Orchestration non bloquante
Dialogue continu avec l’outil Popen ou `asyncio` Adapté aux échanges prolongés

Je fais aussi attention au volume de sortie. Avec `Popen`, attendre naïvement un processus qui remplit un pipe peut conduire à un blocage; la bonne méthode est alors d’utiliser `communicate()`. Avec `asyncio`, l’intérêt est différent: je peux surveiller et exécuter plusieurs sous-processus en même temps, ce qui devient très utile pour des tâches de build ou de scraping concurrent. Dès que la sortie devient volumineuse ou interactive, le choix de l’API change.

Le schéma que j’applique pour automatiser sans surprise

Si je devais résumer ma pratique en une règle de travail, ce serait celle-ci: je garde la commande explicite, je passe les arguments en liste, je capture la sortie seulement si elle me sert réellement, et je pose toujours un `timeout` dès que le risque de blocage existe. Pour les scripts de production ou de CI, j’ajoute presque systématiquement `check=True`, parce qu’un échec silencieux coûte plus cher qu’une exception nette.

Voici le modèle que j’utilise le plus souvent pour une tâche d’automatisation propre et lisible:

import os
import sys
from shutil import which
from subprocess import run

program = which("git")
if program is None:
    raise RuntimeError("Git introuvable")

env = os.environ.copy()
env["PYTHONUTF8"] = "1"

run([program, "status", "--short"], check=True, text=True, env=env)

Ce petit squelette montre l’essentiel: un chemin résolu, un environnement maîtrisé et une exécution qui échoue clairement si quelque chose ne va pas. Dans un projet web, c’est exactement ce genre de base qui permet d’appeler des outils externes sans transformer le script en zone grise. Si tu gardes cette discipline, l’API reste simple à lire et solide à maintenir.

Questions fréquentes

`run()` est idéal pour les commandes simples, synchrones et de courte durée. `Popen` est préférable pour la lecture progressive de sortie, l'interaction continue ou la gestion de processus parallèles.
Passer les arguments en liste (`["git", "status"]`) est plus sûr et prévisible. Python gère l'échappement, évitant les problèmes d'injection de commandes et de parsing, contrairement à une chaîne unique avec `shell=True`.
Utilisez `capture_output=True` avec `subprocess.run()`. Pour obtenir du texte, ajoutez `text=True` ou spécifiez un encodage comme `encoding="utf-8"`. La sortie sera disponible via `result.stdout` et `result.stderr`.
Évitez `shell=True` sauf si vous avez besoin des fonctionnalités du shell (pipes, redirections, jokers). Son utilisation introduit des risques de sécurité (injection) et rend la gestion de l'échappement plus complexe.
Utilisez `check=True` pour lever une `CalledProcessError` en cas de code de retour non nul. Ajoutez `timeout=X` (en secondes) pour lever une `TimeoutExpired` si la commande dépasse le délai imparti.

Évaluer l'article

Moyenne: 0.0 / 5 · 0 évaluations

Tags

subprocess run python subprocess.run exemple exécuter commande externe python subprocess.run shell true gérer sortie subprocess python popen vs subprocess.run
Autor Xavier Moreau
Xavier Moreau
Je m'appelle Xavier Moreau et je cumule 14 ans d'expérience dans le développement web, avec un accent particulier sur JavaScript, le backend, le NoSQL et la sécurité. Mon intérêt pour ces domaines a émergé dès mes débuts dans la programmation, où j'ai découvert la puissance des technologies web et leur capacité à transformer des idées en réalité. J'aime expliquer des concepts complexes de manière accessible, en aidant les lecteurs à naviguer dans les défis techniques qu'ils rencontrent. Au fil des ans, j'ai développé une expertise solide en vérifiant mes sources, en comparant les informations et en simplifiant des sujets parfois ardus. Je m'efforce toujours de fournir des contenus utiles, précis et à jour, en suivant les tendances du secteur et en organisant mes connaissances de manière claire. Mon objectif est d'accompagner les passionnés et les professionnels du développement web dans leur quête de compréhension et d'innovation.

Commentaires (0)

Ajouter un commentaire