subprocess y os: ejecutar comandos del sistema desde Python
subprocess.run() ejecuta comandos del sistema y captura su salida. os provee variables de entorno, rutas del sistema y señales. Juntos permiten automatizar tareas que normalmente harías en la terminal.
subprocess.run() básico
subprocess.run() es la función de alto nivel para ejecutar comandos externos. Espera a que termine y retorna un objeto CompletedProcess con el código de salida y la salida capturada.
import subprocess
# Ejecutar un comando simple
result = subprocess.run(["echo", "Hola desde subprocess"])
print(result.returncode) # 0 (éxito)
# Capturar la salida con capture_output=True
result = subprocess.run(
["python3", "--version"],
capture_output=True,
text=True # stdout/stderr como str, no bytes
)
print(result.stdout.strip()) # Python 3.12.x
# Capturar stdout y stderr por separado
result = subprocess.run(
["ls", "-la", "/tmp"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
lineas = result.stdout.strip().split("
")
print(f"{len(lineas)} entradas en /tmp")
else:
print(f"Error: {result.stderr}")
# Pasar input al proceso con stdin
result = subprocess.run(
["sort"],
input="banana
manzana
fresa
",
capture_output=True,
text=True
)
print(result.stdout) # fresa
manzana
banana (ordenado)0
Python 3.12.3
12 entradas en /tmp
banana
fresa
manzana
capture_output y check para manejo de errores
check=True lanza CalledProcessError si el comando falla (returncode != 0). Es mejor que verificar manualmente returncode cuando quieres que un fallo sea siempre un error.
import subprocess
# check=True: lanza excepción si el comando falla
try:
result = subprocess.run(
["ls", "/directorio/que/no/existe"],
capture_output=True,
text=True,
check=True # lanza CalledProcessError si returncode != 0
)
except subprocess.CalledProcessError as e:
print(f"Comando falló con código {e.returncode}")
print(f"stderr: {e.stderr.strip()}")
# Función helper para ejecutar comandos de forma segura
def run_command(cmd: list[str], cwd: str = None) -> str:
"""Ejecuta un comando y retorna su salida. Lanza si falla."""
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
cwd=cwd # directorio de trabajo
)
return result.stdout.strip()
# Ejecutar git log
try:
git_log = run_command(["git", "log", "--oneline", "-5"])
for linea in git_log.split("
"):
print(linea)
except subprocess.CalledProcessError:
print("No es un repositorio git o git no está instalado")
except FileNotFoundError:
print("git no encontrado en el PATH")
# timeout: matar el proceso si tarda demasiado
try:
result = subprocess.run(
["sleep", "10"],
timeout=2, # segundos
capture_output=True
)
except subprocess.TimeoutExpired:
print("El proceso tardó demasiado — cancelado")Comando falló con código 1
stderr: ls: /directorio/que/no/existe: No such file or directory
abc1234 feat: add feature X
def5678 fix: correct bug Y
El proceso tardó demasiado — canceladosubprocess.run(cmd, shell=True) pasa el comando al shell del sistema — si cmd contiene input del usuario, es una vulnerabilidad de inyección de comandos. Pasa siempre una lista de strings en vez de un string con shell=True.
os.environ para variables de entorno
os.environ es un diccionario con todas las variables de entorno. os.environ.get() es la forma segura de leerlas con valor por defecto.
import os
# Leer variables de entorno
home = os.environ.get("HOME", "/tmp")
path = os.environ.get("PATH", "")
debug = os.environ.get("DEBUG", "false").lower() == "true"
print(f"HOME: {home}")
print(f"DEBUG activo: {debug}")
# Leer con valor requerido (lanzar si no existe)
def require_env(name: str) -> str:
value = os.environ.get(name)
if value is None:
raise EnvironmentError(f"Variable de entorno requerida: {name}")
return value
try:
db_url = require_env("DATABASE_URL")
except EnvironmentError as e:
print(f"Config incompleta: {e}")
db_url = "sqlite:///local.db" # fallback local
# Modificar el entorno para el proceso actual (no persiste)
os.environ["APP_VERSION"] = "1.2.3"
print(os.environ["APP_VERSION"]) # 1.2.3
# Pasar entorno personalizado a subprocess
import subprocess
env = {**os.environ, "MY_VAR": "valor_custom"}
result = subprocess.run(
["python3", "-c", "import os; print(os.environ.get('MY_VAR'))"],
capture_output=True,
text=True,
env=env,
)
print(result.stdout.strip()) # valor_customHOME: /Users/usuario
DEBUG activo: False
Config incompleta: Variable de entorno requerida: DATABASE_URL
1.2.3
valor_customos y shutil para operaciones de archivos
os y shutil complementan a pathlib con operaciones que involucran al sistema operativo: obtener el directorio actual, listar archivos, copiar árboles de directorios.
import os
import shutil
import tempfile
from pathlib import Path
# Directorio actual
print(os.getcwd())
# Listar archivos (os.listdir vs pathlib.iterdir)
entries = os.listdir("/tmp")
print(f"Entradas en /tmp: {len(entries)}")
# os.walk: recorrer árbol de directorios
base = Path(tempfile.mkdtemp())
(base / "a").mkdir()
(base / "b").mkdir()
(base / "a" / "file1.txt").write_text("uno")
(base / "b" / "file2.txt").write_text("dos")
for root, dirs, files in os.walk(base):
nivel = Path(root).relative_to(base)
print(f" {nivel}/ → archivos: {files}")
# shutil: operaciones de alto nivel con archivos
# copiar un archivo
src = base / "a" / "file1.txt"
dst = base / "file1_copia.txt"
shutil.copy2(src, dst) # copy2 preserva metadatos
print(f"Copiado: {dst.exists()}")
# copiar árbol entero
destino_arbol = Path(tempfile.mkdtemp())
shutil.copytree(base / "a", destino_arbol / "a_backup")
print(f"Árbol copiado: {(destino_arbol / 'a_backup').is_dir()}")
# mover (rename entre discos — shutil.move funciona siempre)
shutil.move(str(base / "b" / "file2.txt"), str(base / "file2_movido.txt"))
# obtener espacio en disco
total, used, free = shutil.disk_usage("/")
print(f"Disco /: {free // (1024**3)} GB libres")
shutil.rmtree(base)
shutil.rmtree(destino_arbol)/Users/usuario/proyecto
Entradas en /tmp: 23
./ → archivos: []
a/ → archivos: ['file1.txt']
b/ → archivos: ['file2.txt']
Copiado: True
Árbol copiado: True
Disco /: 120 GB libres