O módulo pty cria pseudo-terminais em sistemas Unix. Um pseudo-terminal possui um lado master controlado pelo programa e um lado slave que se comporta como um terminal real para o processo filho. Isso permite executar aplicações interativas, enviar teclas, capturar saída e testar comportamento de TTY programaticamente.
Essa técnica é útil para ferramentas que exigem terminal controlador, testes de CLIs, gravação de sessões e integração com programas que mudam o formato da saída quando detectam pipe. Porém, pseudo-terminais são dependentes da plataforma e exigem cuidado com EOF, resize, sinais, encoding e encerramento do filho.
Disponibilidade
pty está disponível apenas em Unix. O código é principalmente testado em Linux, FreeBSD e macOS. Outros sistemas POSIX podem ter diferenças.
try:
import pty
except ImportError:
pty = None
Windows possui ConPTY e outras APIs, mas não por meio deste módulo padrão.
Master e slave
openpty() retorna dois file descriptors.
import os
import pty
master_fd, slave_fd = pty.openpty()
try:
print(os.ttyname(slave_fd))
finally:
os.close(master_fd)
os.close(slave_fd)
O processo controlador lê e escreve no master. O processo filho usa o slave como stdin, stdout, stderr e terminal controlador.
Executar subprocesso com openpty()
import os
import pty
import subprocess
master, slave = pty.openpty()
processo = subprocess.Popen(
["python3", "-i"],
stdin=slave,
stdout=slave,
stderr=slave,
close_fds=True,
)
os.close(slave)
try:
os.write(master, b"print(2 + 2)\n")
saida = os.read(master, 4096)
print(saida)
finally:
os.close(master)
processo.terminate()
processo.wait()
Um processo interativo pode emitir prompts em blocos separados e manter execução indefinida. Não presuma que uma leitura contém a resposta completa.
pty.fork()
pty.fork() cria um filho cujo terminal controlador está ligado ao pseudo-terminal.
import os
import pty
pid, fd = pty.fork()
if pid == 0:
os.execvp("sh", ["sh"])
else:
os.write(fd, b"echo pronto\n")
print(os.read(fd, 1024))
No filho, o PID retornado é zero e o fd não é válido. No pai, o retorno contém o PID real e o master fd.
Aviso no macOS
A documentação informa que pty.fork() é inseguro no macOS quando misturado com APIs de sistema de alto nível, incluindo urllib.request. Depois de fork em processo com frameworks complexos, estado interno pode ficar inconsistente.
Prefira iniciar um executável simples imediatamente ou usar uma arquitetura com processo dedicado.
pty.spawn()
spawn(argv) inicia um processo e copia a entrada do terminal atual para o filho e a saída do filho para stdout.
import os
import pty
status = pty.spawn(["bash", "-i"])
codigo = os.waitstatus_to_exitcode(status)
print("Código:", codigo)
O retorno é o status bruto de waitpid(). Use os.waitstatus_to_exitcode() para obter o exit code.
Callbacks de leitura
spawn() aceita master_read e stdin_read. Cada callback recebe um fd e deve retornar bytes.
import os
import pty
captura = bytearray()
def ler_master(fd):
dados = os.read(fd, 1024)
captura.extend(dados)
return dados
status = pty.spawn(["sh", "-c", "printf 'ok\\n'"], ler_master)
Retornar b"" sinaliza EOF e o callback não será chamado novamente.
Risco de loop infinito
A documentação alerta que, se stdin_read retornar EOF mas o filho continuar esperando entrada, spawn() pode ficar em loop para sempre. Em Linux, situação semelhante pode ocorrer quando master_read sinaliza EOF antes do processo terminar.
Defina timeout externo, monitore o PID e encerre o filho quando não houver mais caminho de comunicação.
EOF no pseudo-terminal
EOF de PTY não é igual a pipe comum. Em Linux, ler o master após o slave fechar pode gerar OSError com EIO em vez de retornar bytes vazios.
import errno
try:
dados = os.read(master_fd, 4096)
except OSError as exc:
if exc.errno == errno.EIO:
dados = b""
else:
raise
Teste esse comportamento nos sistemas suportados.
I/O parcial
os.write() pode escrever menos bytes que o solicitado. Crie uma função write-all.
def escrever_tudo(fd, dados):
view = memoryview(dados)
while view:
enviados = os.write(fd, view)
view = view[enviados:]
Em non-blocking, trate BlockingIOError e aguarde prontidão com select.
Leitura sem bloquear
import os
import select
prontos, _, _ = select.select([master_fd], [], [], 1.0)
if prontos:
bloco = os.read(master_fd, 4096)
else:
tratar_timeout()
O guia de select no Python explica multiplexação e buffers.
Prompts não têm protocolo universal
Automatizar uma CLI comparando texto do prompt é frágil. O programa pode mudar idioma, cores, espaçamento ou buffering.
Quando possível, use uma API não interativa, flags de linha de comando, stdin estruturado ou biblioteca oficial. Use PTY somente quando a interação de terminal é parte necessária do teste.
ANSI e cores
Ao detectar TTY, aplicações podem emitir sequências ANSI, cores e movimentos de cursor. A captura será diferente de um pipe.
Não imprima sequências recebidas de fonte não confiável diretamente no terminal. Elas podem alterar título, clipboard ou estado visual.
Encoding
O master fornece bytes. Use decoder incremental porque um caractere UTF-8 pode ser dividido entre leituras.
import codecs
decoder = codecs.getincrementaldecoder("utf-8")("replace")
texto = decoder.decode(bloco)
O encoding pode depender da locale do processo filho.
Tamanho da janela
Algumas aplicações adaptam layout ao número de linhas e colunas. Use termios.tcsetwinsize() no slave ou master, conforme a plataforma.
import termios
termios.tcsetwinsize(master_fd, (24, 80))
Depois de mudar, envie SIGWINCH ao grupo do filho quando necessário.
Grupos de processo
Programas interativos podem criar filhos e grupos. Encerrar apenas o PID principal pode deixar processos órfãos.
Crie uma nova sessão quando apropriado e envie sinais ao process group. Não use PID reutilizado sem confirmar identidade.
Ctrl+C
Escrever b"\x03" no master simula o caractere Ctrl+C quando o slave está em modo que gera sinais.
os.write(master_fd, b"\x03")
Isso não equivale sempre a os.kill(pid, SIGINT). O primeiro segue a disciplina do terminal; o segundo envia sinal diretamente.
Senhas e prompts ocultos
Programas que desativam echo continuarão recebendo dados do master. Não registre automaticamente tudo em logs, pois a captura pode conter senha mesmo que ela não apareça na saída.
Gravar sessão
Um callback de spawn() pode salvar toda a saída, semelhante ao comando Unix script.
with open("sessao.log", "ab") as log:
def gravar(fd):
dados = os.read(fd, 1024)
log.write(dados)
log.flush()
return dados
pty.spawn(["sh"], gravar)
Informe o usuário e proteja o arquivo, porque comandos e segredos podem aparecer.
Testar raw e cbreak
PTY é útil para verificar código dos módulos termios e tty sem alterar o terminal real do runner.
Timeouts
Implemente um deadline monotônico para cada expectativa de prompt. Ao expirar, capture a saída disponível, envie uma terminação gentil e depois force kill se necessário.
Timeout global evita testes presos indefinidamente.
Fechamento correto
Feche o slave no pai assim que o filho estiver iniciado. Caso contrário, o master pode nunca observar EOF porque ainda existe uma referência aberta ao slave.
Feche descriptors em todos os caminhos e aguarde o filho com wait() ou waitpid().
Zombie processes
Um filho encerrado continua como zombie até ser coletado. Sempre chame waitpid(), inclusive após timeout e exceção.
Auditoria
pty.spawn()` gera evento de auditoria pty.spawn com o argv. Ambientes restritos podem registrar ou bloquear a execução.
Segurança
Não passe argv construído diretamente com entrada não confiável. Prefira lista de argumentos sem shell. Restrinja executáveis, ambiente, diretório de trabalho e privilégios.
Captura de terminal pode conter segredos, tokens e dados pessoais. Redija ou descarte conteúdo sensível.
Testes recomendados
Teste saída fragmentada, Unicode, ANSI, EOF com EIO, timeout, filho que ignora entrada, Ctrl+C, resize, processo que cria filhos, senha sem echo, fechamento do slave e diferenças Linux/macOS/BSD.
Erros comuns
Os erros mais frequentes são presumir pipe comum, esquecer de fechar o slave no pai, não coletar o filho, esperar prompt textual sem timeout, registrar senhas, usar pty.fork() com APIs de alto nível no macOS, ignorar escrita parcial e não tratar EIO como possível EOF.
Conclusão
pty permite controlar programas que realmente precisam de um terminal. Ele é valioso para testes e automação de CLIs, mas precisa de protocolo explícito para EOF, timeouts, sinais, resize e cleanup.
Prefira APIs não interativas quando existirem, proteja capturas e teste por plataforma. Consulte a documentação oficial de pty e o manual pty(7).







