pty no Python: automatize terminais Unix

Publicado em: 25/08/2026
Tempo de leitura: 6 minutos
Close-up of a parking payment terminal in an indoor garage in Almere, Netherlands.

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

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    Código Python assíncrono em notebook para inspect.markcoroutinefunction
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    markcoroutinefunction: identifique wrappers async

    Aprenda inspect.markcoroutinefunction no Python para identificar wrappers assíncronos, integrar frameworks e evitar detecção incorreta de corrotinas.

    Ler mais

    Tempo de leitura: 6 minutos
    10/10/2026
    Código Python para percorrer pastas e arquivos com Path.walk
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Path.walk: percorra diretórios com segurança

    Aprenda Path.walk no Python para percorrer diretórios, filtrar arquivos, tratar erros e controlar a travessia com segurança.

    Ler mais

    Tempo de leitura: 6 minutos
    10/10/2026
    Depuração de processo Python em terminal com código
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    pdb -p: depure processos Python em execução

    Aprenda a anexar o pdb a um processo Python em execução, inspecionar pilhas e diagnosticar travamentos com segurança.

    Ler mais

    Tempo de leitura: 6 minutos
    09/10/2026
    Código Python e representação de frações numéricas
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    fractions.from_number: converta números em frações

    Aprenda fractions.from_number no Python para converter números em frações exatas, controlar precisão e evitar arredondamentos inesperados.

    Ler mais

    Tempo de leitura: 5 minutos
    09/10/2026
    Desenvolvedor configurando servidor HTTPS e certificado TLS com Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: crie servidor HTTPS local no Python

    Aprenda HTTPSServer no Python para servir HTTPS localmente, configurar certificados, usar threads e entender limites de segurança.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Arquivos protegidos representando extração segura de TAR com Python
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: extraia TAR com segurança

    Aprenda tarfile extraction_filter no Python para extrair arquivos TAR com validação, segurança e controle de caminhos.

    Ler mais

    Tempo de leitura: 6 minutos
    08/10/2026