Python subprocess: Ejecuta comandos externos desde Python (Guía completa)
Updated on
Los scripts de Python a menudo necesitan llamar a programas externos. Puede que necesites ejecutar un comando de shell para comprimir archivos, invocar git para control de versiones, llamar a una utilidad del sistema como ffmpeg para procesar vídeo o ejecutar un binario compilado como parte de un pipeline de datos. Pero recurrir a os.system() o a hacks estilo “backticks” lleva a código frágil, inseguro e imposible de depurar cuando algo sale mal.
El dolor empeora rápido. La salida desaparece en el vacío porque no tienes forma de capturarla. Los errores pasan en silencio porque se ignora el código de retorno. Un solo nombre de archivo suministrado por un usuario con un espacio o un punto y coma puede convertir tu script inocente en una vulnerabilidad de inyección de shell. Y cuando un subprocess se queda colgado, todo tu programa Python se queda colgado con él: sin timeout, sin recuperación, sin explicación.
El módulo subprocess de Python es la solución estándar. Sustituye funciones antiguas como os.system(), os.popen() y el módulo commands (obsoleto) con una API única y coherente para lanzar procesos, capturar su salida, manejar errores, establecer timeouts y construir pipelines. Esta guía cubre todo lo que necesitas para usarlo de forma efectiva y segura.
Inicio rápido con subprocess.run()
La función subprocess.run(), introducida en Python 3.5, es la forma recomendada de ejecutar comandos externos. Ejecuta un comando, espera a que termine y devuelve un objeto 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 successParámetros clave:
capture_output=Truecaptura stdout y stderr (equivalente astdout=subprocess.PIPE, stderr=subprocess.PIPE)text=Truedecodifica la salida como cadenas en lugar de bytes- El comando se pasa como una lista de strings, donde cada argumento es un elemento separado
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"Entender la lista de comandos vs string
El primer argumento de subprocess.run() puede ser una lista o un string. Esta distinción importa por corrección y seguridad.
Forma de lista (recomendada)
Cada elemento de la lista es un argumento separado. Python los pasa directamente al sistema operativo sin interpretación de 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)Los nombres de archivo con espacios, comillas o caracteres especiales funcionan correctamente porque cada argumento se pasa tal cual:
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
)Forma de string (requiere shell=True)
Pasar un único string requiere shell=True, lo que invoca el shell del sistema (/bin/sh en Unix, cmd.exe en Windows) para interpretar el comando.
import subprocess
# String form requires shell=True
result = subprocess.run(
"ls -la | grep '.py'",
shell=True,
capture_output=True,
text=True
)
print(result.stdout)Esto habilita características del shell como pipes (|), redirecciones (>), globbing (*.py) y expansión de variables de entorno ($HOME). Pero también introduce riesgos de seguridad serios, que cubrimos en la sección de Seguridad más abajo.
Capturar salida
Capturar stdout y stderr por separado
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"Unir stderr en stdout
A veces quieres toda la salida en un solo stream. Usa 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"Descartar salida
Envía la salida a subprocess.DEVNULL para suprimirla:
import subprocess
# Run silently -- discard all output
result = subprocess.run(
["apt-get", "update"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)Salida binaria
Omite text=True para recibir bytes crudos. Útil para datos binarios como imágenes o archivos comprimidos:
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")Manejo de errores
Comprobar códigos de retorno manualmente
Por defecto, subprocess.run() no lanza una excepción cuando un comando falla. Debes comprobar returncode tú mismo:
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}")Excepción automática en fallo con check=True
El parámetro check=True lanza subprocess.CalledProcessError cuando el código de retorno es distinto de cero:
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}")Este es el patrón recomendado para comandos que deberían funcionar siempre. Te obliga a manejar fallos explícitamente en lugar de ignorarlos en silencio.
Manejar “command not found”
Si el ejecutable no existe, Python lanza 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
Los procesos largos o colgados pueden bloquear tu script para siempre. El parámetro timeout (en segundos) mata el proceso y lanza subprocess.TimeoutExpired si no termina a tiempo:
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")Esto es esencial para comandos de red, APIs externas o cualquier proceso que pueda colgarse:
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)Pasar entrada a un proceso
Usa el parámetro input para enviar datos al stdin de un proceso:
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"Esto reemplaza el patrón común de “piping” de datos a través de comandos de 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 de entorno
Por defecto, los subprocesses heredan el entorno actual. Puedes modificarlo:
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"Usa siempre os.environ.copy() como base. Pasar un dict sin el entorno existente elimina todas las variables heredadas, lo que puede romper comandos que dependan de PATH, HOME u otras variables del sistema.
Directorio de trabajo
El parámetro cwd establece el directorio de trabajo para el subprocess:
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: cuándo usar cada uno
subprocess.run() es un wrapper de conveniencia sobre subprocess.Popen. Para la mayoría de casos, run() es suficiente. Usa Popen cuando necesites:
- Streaming de salida en tiempo real (línea por línea a medida que se produce)
- Interactuar con un proceso en ejecución (enviar input, leer output en un bucle)
- Construir pipelines de varios pasos conectando varios procesos
- Ejecución no bloqueante con control manual del ciclo de vida del proceso
Tabla comparativa
| Feature | subprocess.run() | subprocess.Popen | os.system() |
|---|---|---|---|
| Recommended | Yes (Python 3.5+) | Yes (advanced) | No (deprecated pattern) |
| Capture output | Yes (capture_output=True) | Yes (via PIPE) | No |
| Return value | CompletedProcess object | Popen process object | Exit code (int) |
| Timeout support | Yes (timeout param) | Manual (via wait/communicate) | No |
| Error checking | check=True raises exception | Manual | Must parse exit code |
| Input to stdin | input parameter | communicate() or stdin.write() | No |
| Real-time output | No (waits for completion) | Yes (stream line by line) | Output goes to terminal |
| Pipelines | Limited (single command) | Yes (chain multiple Popen) | Yes (via shell string) |
| Security | Safe with list args | Safe with list args | Shell injection risk |
| Shell features | Only with shell=True | Only with shell=True | Always uses shell |
Avanzado: subprocess.Popen
Popen te da control total sobre el ciclo de vida del proceso. El constructor inicia el proceso inmediatamente y devuelve un objeto Popen con el que puedes interactuar.
Uso básico 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)Streaming de salida en tiempo real
A diferencia de run(), Popen te permite leer la salida línea por línea a medida que se produce:
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}")Esto es esencial para comandos de larga duración donde quieres mostrar progreso o registrar salida en tiempo real:
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}")Construir pipelines
Conecta varios comandos encadenando el stdout de un proceso al stdin de otro:
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()}")La llamada p1.stdout.close() después de conectarlo a p2 es importante. Permite que p1 reciba una señal SIGPIPE si p2 termina antes, evitando deadlocks.
Comunicación interactiva con un proceso
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: poder y peligro
Poner shell=True ejecuta el comando a través del shell del sistema, habilitando características del shell pero introduciendo riesgos de seguridad.
Cuándo shell=True es útil
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())El problema de la inyección de shell
Nunca pases input de usuario sin sanitizar a 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 filenameSi debes usar shell=True con valores dinámicos, usa 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
)Pero el enfoque más seguro es evitar shell=True por completo y replicar las características del 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
)Mejores prácticas de seguridad
| Practice | Do | Don't |
|---|---|---|
| Command format | ["cmd", "arg1", "arg2"] | f"cmd {user_input}" with shell=True |
| User input | shlex.quote() if shell is needed | String concatenation into commands |
| Shell mode | shell=False (default) | shell=True with untrusted input |
| Executable path | Full paths like /usr/bin/git | Rely on PATH for security-critical code |
| Input validation | Validate and sanitize before passing | Pass raw user input to commands |
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()Ejemplos del mundo real
Ejecutar comandos de 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")Comprimir y extraer archivos
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")Revisar información del sistema
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())Procesar archivos de datos con herramientas externas
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 despliegue automatizado
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")Consideraciones multiplataforma
Los comandos se comportan de forma diferente en Windows vs Unix. Escribe código 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)Diferencias clave por plataforma:
| Feature | Unix/macOS | Windows |
|---|---|---|
| Shell | /bin/sh | cmd.exe |
| Path separator | / | \\ |
| Built-in commands (dir, copy) | Not available | Require shell=True |
| Executable extension | Not needed | .exe required sometimes |
| Signal handling | Full POSIX signals | Limited |
shlex.quote() | Works | Use subprocess.list2cmdline() |
Ejecutar subprocess en Jupyter Notebooks
Ejecutar comandos de shell desde notebooks de Jupyter es un flujo común para data scientists. Aunque Jupyter soporta la sintaxis !command para llamadas rápidas al shell, subprocess te da manejo correcto de errores y captura de salida dentro de tu código Python.
Al depurar llamadas a subprocess en notebooks —especialmente cuando los comandos fallan en silencio o producen salida inesperada— RunCell (opens in a new tab) puede ayudar. RunCell es un agente de IA para Jupyter que entiende el contexto de tu notebook. Puede diagnosticar por qué falla un comando de subprocess, sugerir los argumentos correctos y manejar particularidades específicas de cada plataforma. En lugar de alternar entre terminal y notebook para depurar un comando de shell, RunCell rastrea el problema directamente en tu celda.
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)Errores comunes y cómo solucionarlos
Error 1: Olvidar capturar la salida
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 outputError 2: Usar un string sin 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)Error 3: Ignorar errores
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}")Error 4: Deadlock con 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() # SafeError 5: No manejar la codificación
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
)Referencia completa de parámetros 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é es el módulo subprocess en Python?
El módulo subprocess es la librería estándar de Python para ejecutar comandos y programas externos desde scripts de Python. Sustituyó enfoques más antiguos como os.system(), os.popen() y el módulo commands. Proporciona funciones para lanzar nuevos procesos, conectarse a sus pipes de entrada/salida/error, obtener códigos de retorno y manejar timeouts. La interfaz principal es subprocess.run() para ejecución simple de comandos y subprocess.Popen para casos avanzados que requieren E/S en tiempo real o pipelines de procesos.
¿Cuál es la diferencia entre subprocess.run() y subprocess.Popen?
subprocess.run() es una función de alto nivel (convenience) que ejecuta un comando, espera a que termine y devuelve un objeto CompletedProcess con la salida. Es la opción correcta para la mayoría de tareas. subprocess.Popen es una clase de menor nivel que te da control directo sobre el proceso: puedes hacer streaming de salida línea por línea, enviar input de forma interactiva, construir pipelines de múltiples procesos y gestionar el ciclo de vida del proceso manualmente. Usa Popen cuando necesites streaming de salida en tiempo real o conectar varios procesos entre sí.
¿Es peligroso shell=True en subprocess?
Sí, usar shell=True con input no confiable crea una vulnerabilidad de inyección de shell. Cuando se establece shell=True, el string del comando se pasa al shell del sistema para su interpretación, lo que significa que metacaracteres del shell como ;, |, && y $() se ejecutan. Un atacante podría inyectar comandos arbitrarios. El valor por defecto seguro es shell=False con comandos pasados como lista. Si debes usar shell=True, sanitiza la entrada con shlex.quote() y nunca pases input de usuario sin procesar.
¿Cómo capturo la salida de un comando ejecutado con subprocess?
Usa capture_output=True y text=True con subprocess.run(). La salida se guarda en result.stdout (como string) y los errores en result.stderr. Por ejemplo: result = subprocess.run(["ls", "-la"], capture_output=True, text=True) y luego accede a result.stdout. Sin text=True, la salida se devuelve como bytes.
¿Cómo manejo timeouts de subprocess en Python?
Pasa el parámetro timeout (en segundos) a subprocess.run(). Si el proceso supera el timeout, Python lo mata y lanza subprocess.TimeoutExpired. Ejemplo: subprocess.run(["slow_command"], timeout=30). Para Popen, usa proc.communicate(timeout=30) o proc.wait(timeout=30). Envuelve siempre el código sensible a timeouts en un bloque try/except.
¿Por qué os.system() sigue funcionando si se recomienda subprocess?
os.system() no está formalmente obsoleto, pero se considera una interfaz legacy. Ejecuta comandos a través del shell (como shell=True), no puede capturar salida, no ofrece mecanismo de timeout y solo devuelve el estado de salida. subprocess.run() hace todo lo que hace os.system(), además de captura de salida, manejo de errores, timeouts y paso seguro de argumentos. Todo el código nuevo debería usar subprocess.
Conclusión
El módulo subprocess es la herramienta definitiva de Python para ejecutar comandos externos. Usa subprocess.run() para la ejecución directa de comandos: maneja captura de salida, comprobación de errores, timeouts y paso de input en una sola llamada. Recurre a subprocess.Popen solo cuando necesites streaming de salida en tiempo real, comunicación interactiva con procesos o pipelines de varios pasos.
El hábito más importante es evitar shell=True con input proporcionado por usuarios. Pasa comandos como listas para eliminar por completo los riesgos de inyección de shell. Usa check=True para detectar fallos pronto. Establece timeout para prevenir procesos colgados. Y usa text=True para trabajar con strings en lugar de bytes crudos.
Desde automatización con git hasta la orquestación de pipelines de datos, subprocess te da el control y la seguridad que os.system() nunca pudo ofrecer. Domina estos patrones y podrás integrar con confianza cualquier herramienta externa en tus flujos de trabajo con Python.