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

    A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    tty no Python: modos raw e cbreak

    Aprenda tty no Python para usar raw e cbreak, ler teclas, tratar sequências, Unicode e restaurar o terminal Unix com

    Ler mais

    Tempo de leitura: 6 minutos
    25/08/2026
    Multiple padlocks securing a green chain link fence, symbolizing safety and protection.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    termios no Python: controle seguro do terminal

    Aprenda termios no Python para modo canônico, echo, leitura por tecla, baud rate, filas e restauração segura do terminal POSIX.

    Ler mais

    Tempo de leitura: 6 minutos
    25/08/2026
    Detailed close-up of a combination lock with numbers in focus, highlighting security and privacy.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    fcntl no Python: locks e controle de arquivos

    Aprenda fcntl no Python para locks, flags de descritores, ioctl, pipes e controle Unix, evitando buffers incorretos e corrupção de

    Ler mais

    Tempo de leitura: 6 minutos
    25/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    readline no Python: histórico e autocomplete

    Aprenda readline no Python para histórico, autocomplete, edição de linha, GNU Readline, libedit e prompts seguros no terminal.

    Ler mais

    Tempo de leitura: 6 minutos
    25/08/2026
    Serene stream flowing through Bavarian mountains, capturing winter beauty and natural tranquility.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    io no Python: domine streams e buffers

    Aprenda io no Python para trabalhar com streams de texto e bytes, buffering, encoding, StringIO, BytesIO e I/O bruto com

    Ler mais

    Tempo de leitura: 7 minutos
    24/08/2026
    Business professional analyzing financial data on multiple computer monitors at his workspace.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    select no Python: monitore vários I/Os

    Aprenda select no Python para monitorar sockets e pipes, tratar leituras parciais, backpressure, epoll, poll e sinais sem busy loop.

    Ler mais

    Tempo de leitura: 7 minutos
    24/08/2026