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







