Skip to content

Python subprocess: Execute comandos externos a partir do Python (Guia completo)

Updated on

Scripts Python frequentemente precisam chamar programas externos. Você pode precisar executar um comando de shell para compactar arquivos, invocar git para controle de versão, chamar um utilitário do sistema como ffmpeg para processar vídeo ou executar um binário compilado como parte de um pipeline de dados. Mas recorrer a os.system() ou a “gambiarras” no estilo de backticks leva a um código frágil, inseguro e impossível de depurar quando algo dá errado.

A dor piora rápido. A saída desaparece no nada porque você não tem como capturá-la. Erros passam silenciosamente porque o código de retorno é ignorado. Um único nome de arquivo fornecido pelo usuário com um espaço ou ponto e vírgula pode transformar seu script inocente em uma vulnerabilidade de injeção de shell. E quando um subprocess trava, todo o seu programa Python trava junto — sem timeout, sem recuperação, sem explicação.

O módulo subprocess do Python é a solução padrão. Ele substitui funções mais antigas como os.system(), os.popen() e o módulo commands (descontinuado) por uma API única e consistente para criar processos, capturar a saída, tratar erros, definir timeouts e construir pipelines. Este guia cobre tudo o que você precisa para usá-lo de forma eficaz e segura.

📚

Início rápido com subprocess.run()

A função subprocess.run(), introduzida no Python 3.5, é a forma recomendada de executar comandos externos. Ela executa um comando, espera ele terminar e retorna um 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 success

Parâmetros principais:

  • capture_output=True captura stdout e stderr (equivalente a stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  • text=True decodifica a saída como strings em vez de bytes
  • O comando é passado como uma lista de strings, onde cada argumento é um 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"

Entendendo a lista de comando vs string

O primeiro argumento de subprocess.run() pode ser uma lista ou uma string. Essa distinção importa para correção e segurança.

Forma em lista (recomendada)

Cada elemento da lista é um argumento separado. O Python os passa diretamente ao sistema operacional sem interpretação do 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)

Nomes de arquivos com espaços, aspas ou caracteres especiais funcionam corretamente porque cada argumento é passado “como está”:

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 em string (exige shell=True)

Passar uma única string exige shell=True, que invoca o shell do sistema (/bin/sh no Unix, cmd.exe no Windows) para interpretar o 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)

Isso habilita recursos do shell como pipes (|), redirecionamentos (>), globbing (*.py) e expansão de variáveis de ambiente ($HOME). Mas também introduz riscos sérios de segurança, que cobrimos na seção de Segurança abaixo.

Capturando saída

Capturar stdout e stderr separadamente

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"

Mesclar stderr em stdout

Às vezes você quer toda a saída em um único fluxo. Use 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 saída

Envie a saída para subprocess.DEVNULL para suprimí-la:

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

Saída binária

Omita text=True para receber bytes brutos. Útil para dados binários como imagens ou arquivos compactados:

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")

Tratamento de erros

Verificar códigos de retorno manualmente

Por padrão, subprocess.run() não lança uma exceção quando um comando falha. Você precisa verificar returncode por conta própria:

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}")

Exceção automática em falha com check=True

O parâmetro check=True lança subprocess.CalledProcessError quando o código de retorno é diferente de zero:

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}")

Esse é o padrão recomendado para comandos que sempre deveriam ter sucesso. Ele força você a lidar com falhas explicitamente, em vez de ignorá-las silenciosamente.

Lidar com comando não encontrado

Se o executável não existir, o Python lança 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

Processos demorados ou travados podem bloquear seu script para sempre. O parâmetro timeout (em segundos) encerra o processo e lança subprocess.TimeoutExpired se ele não terminar a tempo:

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")

Isso é essencial para comandos de rede, APIs externas ou qualquer processo que possa travar:

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)

Passando entrada para um processo

Use o parâmetro input para enviar dados para stdin de um processo:

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"

Isso substitui o padrão comum de fazer pipe de dados via comandos do 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"

Variáveis de ambiente

Por padrão, subprocessos herdam o ambiente atual. Você pode modificá-lo:

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"

Sempre use os.environ.copy() como base. Passar um dict sem o ambiente existente remove todas as variáveis herdadas, o que pode quebrar comandos que dependem de PATH, HOME ou outras variáveis do sistema.

Diretório de trabalho

O parâmetro cwd define o diretório de trabalho do subprocesso:

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: quando usar cada um

subprocess.run() é um wrapper de conveniência em torno de subprocess.Popen. Para a maioria dos casos, run() é suficiente. Use Popen quando você precisa de:

  • Streaming de saída em tempo real (linha por linha conforme é produzido)
  • Interagir com um processo em execução (enviar input, ler output em loop)
  • Construir pipelines em múltiplas etapas conectando vários processos
  • Execução não bloqueante com controle manual do ciclo de vida do processo

Tabela de comparação

Recursosubprocess.run()subprocess.Popenos.system()
RecomendadoSim (Python 3.5+)Sim (avançado)Não (padrão legado)
Capturar saídaSim (capture_output=True)Sim (via PIPE)Não
Valor de retornoObjeto CompletedProcessObjeto de processo PopenCódigo de saída (int)
Suporte a timeoutSim (timeout param)Manual (via wait/communicate)Não
Verificação de errocheck=True lança exceçãoManualPrecisa interpretar o exit code
Entrada para stdinParâmetro inputcommunicate() ou stdin.write()Não
Saída em tempo realNão (espera concluir)Sim (stream linha a linha)Saída vai para o terminal
PipelinesLimitado (comando único)Sim (encadear múltiplos Popen)Sim (via string de shell)
SegurançaSeguro com args em listaSeguro com args em listaRisco de injeção de shell
Recursos do shellSó com shell=TrueSó com shell=TrueSempre usa shell

Avançado: subprocess.Popen

Popen dá controle total sobre o ciclo de vida do processo. O construtor inicia o processo imediatamente e retorna um objeto Popen com o qual você pode interagir.

Uso básico do 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 saída em tempo real

Diferente do run(), o Popen permite ler a saída linha por linha conforme ela é produzida:

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}")

Isso é essencial para comandos longos em que você quer mostrar progresso ou registrar logs em tempo 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}")

Construindo pipelines

Conecte múltiplos comandos fazendo pipe do stdout de um processo para o stdin de outro:

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()}")

A chamada p1.stdout.close() após conectá-lo ao p2 é importante. Ela permite que p1 receba um sinal SIGPIPE se p2 sair mais cedo, prevenindo deadlocks.

Comunicação interativa com processo

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 e perigo

Definir shell=True executa o comando através do shell do sistema, habilitando recursos do shell, mas introduzindo riscos de segurança.

Quando shell=True é ú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())

O problema de injeção de shell

Nunca passe input do usuário sem sanitização para 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

Se você realmente precisar usar shell=True com valores dinâmicos, use 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
)

Mas a abordagem mais segura é evitar shell=True por completo e reproduzir os recursos do shell em 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
    )

Boas práticas de segurança

PráticaFaçaNão faça
Formato de comando["cmd", "arg1", "arg2"]f"cmd {user_input}" com shell=True
Input do usuárioshlex.quote() se o shell for necessárioConcatenar strings em comandos
Modo shellshell=False (padrão)shell=True com input não confiável
Caminho do executávelCaminhos completos como /usr/bin/gitConfiar no PATH em código crítico de segurança
Validação de inputValidar e sanitizar antes de passarPassar input bruto do usuário para comandos
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()

Exemplos do mundo real

Executar comandos do 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")

Compactar e extrair arquivos

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")

Verificar informações do 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())

Processar arquivos de dados com ferramentas 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 deploy 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")

Considerações cross-platform

Comandos se comportam de forma diferente no Windows vs Unix. Escreva código portátil:

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)

Principais diferenças por plataforma:

RecursoUnix/macOSWindows
Shell/bin/shcmd.exe
Separador de caminho/\\
Comandos built-in (dir, copy)Não disponíveisExigem shell=True
Extensão de executávelNão necessáriaÀs vezes .exe é necessária
Manipulação de sinaisSinais POSIX completosLimitado
shlex.quote()FuncionaUse subprocess.list2cmdline()

Executando subprocess em Jupyter Notebooks

Executar comandos de shell a partir de notebooks Jupyter é um fluxo comum para cientistas de dados. Embora o Jupyter suporte a sintaxe !command para chamadas rápidas ao shell, subprocess te dá tratamento de erros adequado e captura de saída dentro do seu código Python.

Ao depurar chamadas de subprocess em notebooks — especialmente quando comandos falham silenciosamente ou produzem saída inesperada — RunCell (opens in a new tab) pode ajudar. O RunCell é um agente de IA para Jupyter que entende o contexto do seu notebook. Ele pode diagnosticar por que um comando de subprocess falha, sugerir os argumentos corretos e lidar com particularidades específicas de plataforma. Em vez de alternar entre terminal e notebook para depurar um comando de shell, o RunCell rastreia o problema diretamente na sua célula.

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)

Erros comuns e como corrigir

Erro 1: Esquecer de capturar a saída

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

Erro 2: Usar uma string sem 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)

Erro 3: Ignorar erros

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}")

Erro 4: Deadlock com 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

Erro 5: Não lidar com encoding

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
)

Referência completa de parâmetros do 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

O que é o módulo subprocess no Python?

O módulo subprocess é a biblioteca padrão do Python para executar comandos e programas externos a partir de scripts Python. Ele substituiu abordagens antigas como os.system(), os.popen() e o módulo commands. Ele fornece funções para criar novos processos, conectar aos pipes de entrada/saída/erro, obter códigos de retorno e lidar com timeouts. A interface principal é subprocess.run() para execução simples de comandos, e subprocess.Popen para casos avançados que exigem E/S em tempo real ou pipelines de processos.

Qual é a diferença entre subprocess.run() e subprocess.Popen?

subprocess.run() é uma função de alto nível e de conveniência que executa um comando, espera ele terminar e retorna um objeto CompletedProcess com a saída. É a escolha certa para a maioria das tarefas. subprocess.Popen é uma classe de nível mais baixo que te dá controle direto sobre o processo: você pode fazer streaming da saída linha por linha, enviar input de forma interativa, construir pipelines com múltiplos processos e gerenciar manualmente o ciclo de vida do processo. Use Popen quando precisar de streaming de saída em tempo real ou ao conectar vários processos.

shell=True é perigoso no subprocess?

Sim, usar shell=True com input não confiável cria uma vulnerabilidade de injeção de shell. Quando shell=True está definido, a string do comando é passada ao shell do sistema para interpretação, o que significa que metacaracteres do shell como ;, |, && e $() são executados. Um atacante poderia injetar comandos arbitrários. O padrão seguro é shell=False com comandos passados como lista. Se você precisar usar shell=True, sanitize o input com shlex.quote() e nunca passe input bruto do usuário.

Como capturo a saída de um comando subprocess?

Use capture_output=True e text=True com subprocess.run(). A saída fica em result.stdout (como string) e os erros em result.stderr. Por exemplo: result = subprocess.run(["ls", "-la"], capture_output=True, text=True) e então acesse result.stdout. Sem text=True, a saída é retornada como bytes.

Como lidar com timeouts de subprocess no Python?

Passe o parâmetro timeout (em segundos) para subprocess.run(). Se o processo exceder o timeout, o Python o encerra e lança subprocess.TimeoutExpired. Exemplo: subprocess.run(["slow_command"], timeout=30). Para Popen, use proc.communicate(timeout=30) ou proc.wait(timeout=30). Sempre envolva código sensível a timeout em um bloco try/except.

Por que os.system() ainda funciona se subprocess é recomendado?

os.system() não é formalmente descontinuado, mas é considerado uma interface legada. Ele executa comandos via shell (como shell=True), não consegue capturar saída, não tem mecanismo de timeout e retorna apenas o status de saída. subprocess.run() faz tudo o que os.system() faz, além de captura de saída, tratamento de erros, timeouts e passagem segura de argumentos. Todo código novo deve usar subprocess.

Conclusão

O módulo subprocess é a ferramenta definitiva do Python para executar comandos externos. Use subprocess.run() para execução direta de comandos — ele lida com captura de saída, verificação de erros, timeouts e envio de input em uma única chamada de função. Recorra a subprocess.Popen apenas quando você precisar de streaming de saída em tempo real, comunicação interativa com o processo ou pipelines em múltiplas etapas.

O hábito mais importante é evitar shell=True com input fornecido pelo usuário. Passe comandos como listas para eliminar completamente riscos de injeção de shell. Use check=True para detectar falhas cedo. Defina timeout para evitar processos travados. E use text=True para trabalhar com strings em vez de bytes brutos.

De automação do git a orquestração de pipelines de dados, subprocess oferece o controle e a segurança que os.system() nunca poderia oferecer. Domine esses padrões e você poderá integrar com confiança qualquer ferramenta externa aos seus fluxos de trabalho em Python.

📚