pty en Python: automatiza terminales Unix

Publicado el: 25/08/2026
Tempo de leitura: 5 minutos
Close-up of a person using a metro card machine for public transport payment indoors.

El módulo pty crea pseudo-terminales en Unix. Un pseudo-terminal tiene un lado master controlado por el programa y un lado slave que parece un terminal real para el proceso hijo. Esto permite enviar teclas, capturar salida y probar comportamiento dependiente de TTY.

Los pseudo-terminales sirven para tests de CLIs, grabación de sesiones, wrappers y programas que cambian su salida al detectar un pipe. También dependen mucho de la plataforma y requieren cuidado con EOF, resize, señales, encoding, timeouts y cleanup de procesos.

Disponibilidad

pty solo existe en Unix. Está probado principalmente en Linux, FreeBSD y macOS. Otros sistemas POSIX pueden comportarse distinto.

try:
    import pty
except ImportError:
    pty = None

Windows ofrece ConPTY y otras APIs, pero no mediante este módulo estándar.

Master y slave

openpty() devuelve dos 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)

El controlador lee y escribe en master. El hijo usa slave como stdin, stdout, stderr y terminal controlador.

Ejecutar subprocess con openpty()

import os
import pty
import subprocess

master, slave = pty.openpty()
proceso = 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")
    salida = os.read(master, 4096)
    print(salida)
finally:
    os.close(master)
    proceso.terminate()
    proceso.wait()

La salida interactiva llega fragmentada y el proceso puede seguir vivo. Una lectura no garantiza respuesta completa.

pty.fork()

pty.fork() crea un hijo conectado al pseudo-terminal.

import os
import pty

pid, fd = pty.fork()
if pid == 0:
    os.execvp("sh", ["sh"])
else:
    os.write(fd, b"echo listo\n")
    print(os.read(fd, 1024))

En el hijo, PID es cero y el fd no es válido. En el padre, se devuelve el PID real y el master fd.

Aviso de macOS

La documentación advierte que pty.fork() es inseguro en macOS cuando se mezcla con APIs de sistema de alto nivel, incluido urllib.request. El estado de frameworks puede quedar inconsistente después del fork.

Ejecuta inmediatamente un programa simple o aísla el trabajo en un proceso dedicado.

pty.spawn()

spawn(argv) inicia un proceso y copia entrada del terminal actual al hijo y salida del hijo a stdout.

import os
import pty

status = pty.spawn(["bash", "-i"])
codigo = os.waitstatus_to_exitcode(status)
print("Código:", codigo)

El retorno es el status bruto de waitpid().

Callbacks de lectura

spawn() acepta master_read y stdin_read. Cada uno recibe un fd y devuelve bytes.

import os
import pty

captura = bytearray()

def leer_master(fd):
    datos = os.read(fd, 1024)
    captura.extend(datos)
    return datos

status = pty.spawn(["sh", "-c", "printf 'ok\\n'"], leer_master)

Devolver b"" señala EOF.

Riesgo de loop infinito

Si stdin_read devuelve EOF mientras el hijo sigue esperando entrada, spawn() puede quedar en loop. Algo parecido puede ocurrir en Linux si master_read señala EOF antes de que el hijo termine.

Usa timeout externo, monitoriza el PID y termina al hijo cuando ya no hay comunicación.

EOF en PTY

EOF no siempre se comporta como un pipe. En Linux, leer master después de cerrar slave puede lanzar OSError con EIO.

import errno

try:
    datos = os.read(master_fd, 4096)
except OSError as exc:
    if exc.errno == errno.EIO:
        datos = b""
    else:
        raise

Prueba la plataforma real.

Escrituras parciales

os.write() puede escribir menos bytes.

def escribir_todo(fd, datos):
    view = memoryview(datos)
    while view:
        enviados = os.write(fd, view)
        view = view[enviados:]

En non-blocking, maneja BlockingIOError y espera readiness.

Leer sin bloquear

import os
import select

listos, _, _ = select.select([master_fd], [], [], 1.0)
if listos:
    bloque = os.read(master_fd, 4096)
else:
    tratar_timeout()

select en Python explica multiplexación y buffers.

Los prompts no son protocolos estables

Automatizar una CLI comparando texto es frágil. Idioma, colores, espacios, versión y buffering pueden cambiar.

Prefiere una API no interactiva, flags, stdin estructurado o biblioteca oficial. Usa PTY solo cuando el comportamiento de terminal sea imprescindible.

ANSI y colores

Al detectar TTY, el hijo puede emitir colores ANSI y movimientos de cursor. La captura será distinta de un pipe.

No reproduzcas secuencias no confiables directamente en el terminal.

Encoding

Master entrega bytes. Usa decoder incremental porque un carácter UTF-8 puede dividirse entre lecturas.

import codecs

decoder = codecs.getincrementaldecoder("utf-8")("replace")
texto = decoder.decode(bloque)

La locale del hijo determina el encoding esperado.

Tamaño de ventana

Programas interactivos adaptan layout a filas y columnas. Usa termios.tcsetwinsize() cuando esté disponible.

import termios

termios.tcsetwinsize(master_fd, (24, 80))

Después del resize, envía SIGWINCH al grupo si es necesario.

Grupos de procesos

Programas interactivos pueden crear hijos. Terminar solo el PID principal deja procesos huérfanos.

Crea una sesión y envía señales al grupo cuando corresponda.

Ctrl+C

Escribir b"\x03" simula Ctrl+C cuando la disciplina del slave lo convierte en SIGINT.

os.write(master_fd, b"\x03")

No es igual que os.kill(pid, SIGINT), que envía señal directa.

Contraseñas

El hijo puede desactivar echo, pero master sigue enviando el secreto. No registres automáticamente todas las escrituras.

Grabar sesión

Un callback puede guardar salida como el comando Unix script.

with open("sesion.log", "ab") as log:
    def grabar(fd):
        datos = os.read(fd, 1024)
        log.write(datos)
        log.flush()
        return datos
    pty.spawn(["sh"], grabar)

Informa al usuario y protege el archivo.

Probar raw y cbreak

PTY sirve para probar termios y tty sin alterar el terminal real.

Timeouts

Usa deadline monotónico para cada prompt. Al expirar, captura salida, envía terminación gentil y fuerza kill si es necesario.

Cierre correcto

Cierra slave en el padre al iniciar el hijo. Si queda abierto, master puede no observar EOF.

Cierra todos los descriptors y espera al hijo con wait() o waitpid().

Zombies

Un hijo terminado queda zombie hasta ser recolectado. Llama siempre waitpid(), incluso tras errores.

Auditoría

pty.spawn() genera evento pty.spawn con argv. Entornos restringidos pueden bloquearlo.

Seguridad

No construyas argv directamente con entrada no confiable. Usa lista sin shell y restringe ejecutables, entorno, directorio y privilegios.

Las capturas pueden contener tokens y datos personales. Redacta contenido sensible.

Pruebas recomendadas

Prueba salida fragmentada, Unicode, ANSI, EIO como EOF, timeouts, hijo que espera siempre, Ctrl+C, resize, árbol de procesos, contraseñas, cierre de slave y diferencias Linux/macOS/BSD.

Errores comunes

Los fallos frecuentes son tratar PTY como pipe, dejar slave abierto, no recolectar al hijo, esperar prompt sin timeout, registrar secretos, usar pty.fork() con APIs de alto nivel en macOS, ignorar escritura parcial y no tratar EIO como EOF.

Conclusión

pty controla programas que realmente necesitan un terminal. Es útil para tests y automatización de CLIs, pero necesita reglas claras para EOF, timeouts, señales, resize y cleanup.

Prefiere APIs no interactivas cuando existan, protege transcripciones y prueba por plataforma. Consulta la documentación oficial de pty y pty(7).

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    tty en Python: modos raw y cbreak

    Aprende tty en Python para modos raw y cbreak, lectura de teclas, secuencias, Unicode y restauración segura del terminal Unix.

    Ler mais

    Tempo de leitura: 5 minutos
    25/08/2026
    Laptop displaying code with reflection, perfect for tech and programming themes.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    termios en Python: control seguro del terminal

    Aprende termios en Python para modo canónico, echo, lectura de teclas, baud rate, colas, ventana y restauración segura del TTY.

    Ler mais

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

    fcntl en Python: locks y control de archivos

    Aprende fcntl en Python para locks, flags de descriptors, ioctl, pipes y control Unix, evitando buffers inválidos y corrupción de

    Ler mais

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

    readline en Python: historial y autocomplete

    Aprende readline en Python para historial, autocompletado, edición de línea, GNU Readline, libedit y prompts seguros en terminal.

    Ler mais

    Tempo de leitura: 5 minutos
    25/08/2026
    Vibrant green tree python elegantly coiled on branch, showcasing its natural beauty.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    io en Python: domina streams y buffers

    Aprende io en Python para streams de texto y bytes, buffering, encoding, StringIO, BytesIO, I/O bruto e interfaces file-like.

    Ler mais

    Tempo de leitura: 6 minutos
    24/08/2026
    Vibrant green tree python elegantly coiled on branch, showcasing its natural beauty.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    select en Python: monitorea varios I/O

    Aprende select en Python para monitorear sockets y pipes, tratar I/O parcial, backpressure, poll, epoll, señales y diferencias de plataforma.

    Ler mais

    Tempo de leitura: 6 minutos
    24/08/2026