subprocess
subprocess lanza programas externos, captura stdout/stderr y gestiona códigos de salida. Prefiera listas de argumentos sin shell a menos que las características de shell se requieran explícitamente.
Receta
import subprocess
result = subprocess.run(
["python", "--version"],
capture_output=True,
text=True,
check=True,
)
print(result.stdout.strip())Cuándo usar esto:
- Envoltorios CLI para Git, ffmpeg, openssl
- Herramientas heredadas sin API de Python
- Scripts de empaquetado que invocan compiladores
- Comprobaciones de estado que llaman al binario curl
- Aislamiento en sandbox a través de un proceso separado
Ejemplo funcional
import subprocess
from pathlib import Path
def run_git(args: list[str], cwd: Path) -> str:
completed = subprocess.run(
["git", *args],
cwd=cwd,
capture_output=True,
text=True,
check=False,
)
if completed.returncode != 0:
raise RuntimeError(completed.stderr.strip() or "git falló")
return completed.stdout.strip()
def safe_ls(path: Path) -> str:
# Nunca shell=True con entrada del usuario
proc = subprocess.run(
["ls", "-la", str(path)],
capture_output=True,
text=True,
timeout=5,
)
proc.check_returncode()
return proc.stdout
print(run_git(["--version"], Path.cwd()))Lo que esto demuestra:
- Los argumentos en formato de lista evitan la inyección de shell
check_returncodeocheck=Truepara fallostimeoutpreviene que los hijos se queden colgadostext=Truedecodifica bytes a str con la codificación predeterminada
Inmersión Profunda
run vs Popen
| API | Uso |
|---|---|
run | Espera de una sola vez hasta la finalización |
Popen | IO en streaming, en segundo plano |
IO
capture_output=Trueredirige stdout/stderrstdin=subprocess.DEVNULLcuando no hay entrada
Errores comunes
- shell=True con entrada del usuario - inyección de comandos. Solución: usar listas de argumentos,
shlex.quotesi se requiere shell. - Salida ilimitada - agotamiento de memoria. Solución: hacer streaming con Popen iter o redirigir a un archivo.
- Falta de timeout - cuelgues de zombis. Solución: usar siempre timeout en servicios.
- Confianza en CWD - binarios relativos desde un directorio incorrecto. Solución:
cwdyenvexplícitos. - Bucle de eventos bloqueante - subprocess en ruta asíncrona. Solución:
asyncio.create_subprocess_exec.
Alternativas
| Alternativa | Usar cuándo | No usar cuándo |
|---|---|---|
| shutil.which | Resolver la ruta del binario | No se necesita ejecutar |
| asyncio subprocess | Streaming asíncrono | Script síncrono |
| Librería Pure Python | Existe API estable | CLI puntual está bien |
Preguntas frecuentes
¿shell=True alguna vez?
Solo comandos fijos de confianza en scripts controlados - nunca con entrada externa.
¿Código de retorno 0?
Convención de éxito - verificar explícitamente incluso si no se usa check=True.
¿Fusionar stderr?
stderr=subprocess.STDOUT en el pipe de stdout.
¿Salida binaria?
Omitir text=True; manejar bytes y decodificar explícitamente.
¿Windows?
creationflags para nueva consola; las listas de argumentos siguen siendo preferidas.
¿Entorno?
env={**os.environ, "K": "V"} sobreescritura parcial.
¿Proceso de larga duración?
Popen + communicate con timeout o bucle de sondeo.
¿Auditoría de seguridad?
Marcar cualquier shell=True y concatenación de cadenas en la revisión de seguridad.
¿uv run?
["uv", "run", "python", "-m", "tool"] en formato de lista para un entorno reproducible.
¿pytest?
Usar fixtures con tmp_path; mockear subprocess.run para pruebas unitarias.
Relacionado
- pathlib & os - rutas cwd
- argparse & configparser - herramientas CLI que envuelven subprocess
- Buenas prácticas de la Biblioteca Estándar - seguridad
- Conceptos básicos de concurrencia - bloqueo vs asíncrono
Versiones de la pila: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.