El módulo curses permite crear interfaces de texto avanzadas en terminales de celdas, con ventanas, colores, menús, teclado, mouse, actualización parcial y adaptación al tamaño de pantalla. Utiliza curses o ncurses en lugar de secuencias ANSI escritas manualmente.
Es una buena opción para dashboards locales, monitores de procesos, instaladores, gestores de archivos, herramientas administrativas y aplicaciones usadas por SSH. Para que el programa sea confiable, debe restaurar el terminal después de errores, respetar las capacidades de terminfo y manejar Unicode, resize y pantallas pequeñas.
Disponibilidad
curses es un módulo opcional disponible normalmente en Unix. No está soportado en Android, iOS ni WASI. En Windows puede ser necesaria una implementación compatible de terceros.
try:
import curses
except ImportError:
curses = None
La aplicación debería ofrecer un modo alternativo o un mensaje claro cuando curses no exista.
Comienza con wrapper()
curses.wrapper() inicializa la biblioteca, activa cbreak, desactiva echo, habilita keypad, inicializa colores si están disponibles y restaura el terminal al salir, incluso cuando la función principal lanza una excepción.
import curses
def main(stdscr):
stdscr.clear()
stdscr.addstr(0, 0, "Hola, terminal")
stdscr.refresh()
stdscr.getch()
curses.wrapper(main)
Prefiere wrapper() a llamar manualmente initscr() y endwin(). El traceback aparece después de restaurar el terminal.
Coordenadas
Curses usa (y, x): fila primero y columna después. La esquina superior izquierda es (0, 0).
altura, ancho = stdscr.getmaxyx()
y = altura // 2
x = ancho // 2
stdscr.addstr(y, max(0, x - 5), "Centrado")
Confundir x e y suele provocar escrituras fuera de la ventana.
Limita cada escritura
addstr() y addch() generan curses.error al dibujar fuera de la ventana. Escribir en la celda inferior derecha también puede lanzar una excepción después de pintar.
def escribir_seguro(win, y, x, texto, attr=0):
altura, ancho = win.getmaxyx()
if not (0 <= y < altura and 0 <= x < ancho):
return
disponible = max(0, ancho - x - 1)
if disponible:
win.addnstr(y, x, texto, disponible, attr)
Trata un terminal pequeño como un estado normal, no como un crash.
Loop de eventos
def main(stdscr):
stdscr.keypad(True)
while True:
stdscr.erase()
stdscr.addstr(0, 0, "Pulsa q para salir")
stdscr.refresh()
tecla = stdscr.getch()
if tecla in (ord("q"), ord("Q")):
break
No ejecutes trabajo largo dentro del loop sin permitir redraw y cancelación.
Teclas especiales
Con keypad(True), las secuencias del terminal se convierten en constantes como KEY_UP, KEY_DOWN, KEY_LEFT y KEY_RIGHT.
if tecla == curses.KEY_UP:
seleccion = max(0, seleccion - 1)
elif tecla == curses.KEY_DOWN:
seleccion = min(len(elementos) - 1, seleccion + 1)
No dependas de una tecla poco común como única forma de acceder a una función.
getch(), getkey() y get_wch()
getch() devuelve un entero. getkey() devuelve texto o nombre de tecla. get_wch() es mejor para Unicode: devuelve un carácter para texto y un entero para teclas especiales.
valor = stdscr.get_wch()
if isinstance(valor, str):
procesar_texto(valor)
elif valor == curses.KEY_RESIZE:
necesita_redibujar = True
Configura locale antes de inicializar curses.
Entrada no bloqueante
nodelay(True) hace que getch() devuelva -1 si no hay entrada. timeout(ms) espera un tiempo limitado.
stdscr.timeout(100)
while True:
tecla = stdscr.getch()
actualizar_metricas()
if tecla == ord("q"):
break
No crees busy loop con timeout cero y sin pausa o trabajo útil.
halfdelay()
halfdelay(tenths) espera entre 0,1 y 25,5 segundos y genera error si no llega una tecla. window.timeout() suele integrarse mejor.
Tiempo de Escape
set_escdelay(ms) controla cuánto espera curses después de ESC para distinguir Escape de una secuencia de función. Ajusta el valor para terminal local y SSH lento.
Ventanas
newwin() crea regiones independientes.
altura, ancho = stdscr.getmaxyx()
menu = curses.newwin(altura - 2, 30, 1, 0)
contenido = curses.newwin(altura - 2, ancho - 30, 1, 30)
menu.box()
contenido.box()
Las subventanas comparten memoria con sus padres, por lo que el orden de dibujo y refresh debe coordinarse.
Pads
newpad() crea un área mayor que la pantalla, útil para logs o documentos.
pad = curses.newpad(1000, 200)
for indice in range(1000):
pad.addstr(indice, 0, f"Línea {indice}")
pad.refresh(offset, 0, 1, 0, altura - 2, ancho - 1)
Valida tanto el rectángulo virtual como el visible.
Actualizar varias ventanas
Múltiples refresh() pueden aumentar flicker. Usa noutrefresh() en cada ventana y una sola llamada a doupdate().
menu.noutrefresh()
contenido.noutrefresh()
curses.doupdate()
Colores
Llama start_color() y comprueba has_colors().
if curses.has_colors():
curses.start_color()
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
stdscr.addstr(0, 0, "OK", curses.color_pair(1))
COLORS y COLOR_PAIRS están disponibles después de inicializar colores.
Colores predeterminados en Python 3.14
assume_default_colors(fg, bg), añadido en Python 3.14, permite utilizar foreground y background predeterminados y conservar transparencia.
if hasattr(curses, "assume_default_colors"):
curses.assume_default_colors(-1, -1)
curses.init_pair(1, curses.COLOR_CYAN, -1)
Incluye fallback para versiones anteriores y terminales sin soporte.
Atributos visuales
A_BOLD, A_REVERSE, A_UNDERLINE y A_DIM pueden combinarse con colores.
No comuniques estado solo por color. Algunos terminales no soportan todos los atributos y algunos usuarios no distinguen ciertos colores.
Unicode y ancho visual
La longitud de un string Python no siempre coincide con las celdas visibles. Ideogramas pueden ocupar dos columnas y marcas combinantes cero.
Usa una biblioteca como wcwidth para alineación robusta y prueba emojis y caracteres combinados.
Encoding de la ventana
Cada ventana posee encoding, normalmente derivado de la locale.
import locale
locale.setlocale(locale.LC_ALL, "")
Configura locale una vez antes de curses, no durante el loop.
Resize
Un cambio de tamaño puede producir KEY_RESIZE. Consulta dimensiones, reconstruye layout y redibuja desde el modelo.
if tecla == curses.KEY_RESIZE:
curses.update_lines_cols()
stdscr.erase()
reconstruir_layout(stdscr)
resizeterm() actualiza ventanas estándar; los pads requieren lógica propia.
Dimensiones mínimas
Define un tamaño mínimo y muestra un mensaje simple cuando el terminal sea demasiado pequeño.
altura, ancho = stdscr.getmaxyx()
if altura < 10 or ancho < 40:
escribir_seguro(stdscr, 0, 0, "Aumenta el terminal")
stdscr.refresh()
return
Mouse
mousemask() activa eventos. Después de KEY_MOUSE, llama getmouse().
El soporte cambia entre terminales, tmux, screen y SSH. Siempre ofrece navegación por teclado.
Campos de texto
curses.textpad.Textbox ofrece edición similar a Emacs. Valida y limita el contenido final.
Python 3.14 aumentó el máximo de getstr() e instr() de 1023 a 2047 caracteres, pero la aplicación debería imponer límites menores.
Manejo de curses.error
Pantallas pequeñas y escrituras en límites generan curses.error. Captura casos esperados localmente, pero evita un except curses.error: pass global.
TERM y terminfo
Curses utiliza la entrada terminfo seleccionada por TERM. Un valor incorrecto provoca teclas, colores y cursor rotos.
No fuerces xterm-256color sin comprobar el terminal. Corrige el entorno o instala la entrada terminfo correcta.
No mezcles ANSI directo
Escribir escapes manuales mientras curses está activo puede desincronizar la pantalla virtual y física. Usa las funciones de curses para cursor, atributos y limpieza.
Subprocesses
Antes de ejecutar un programa externo interactivo, restaura temporalmente el modo shell o cierra y recrea la interfaz.
Para automatización aislada consulta pty en Python.
Relación con tty y termios
Curses gestiona cbreak, echo, keypad y detalles sobre las APIs explicadas en tty en Python y termios en Python.
No cambies flags termios por fuera mientras curses está activo sin comprender su estado interno.
Threads
Mantén todas las llamadas curses en un solo thread. Workers pueden publicar datos por una cola y el thread de UI los dibuja.
Cierre cooperativo
Usa una flag o evento, abandona el loop normalmente y deja que wrapper() restaure el terminal.
Los signal handlers deben ser mínimos. signal en Python explica cómo despertar el loop.
Sanitizar texto no confiable
Datos remotos pueden contener controles, newlines, tabs y escapes.
def limpiar_texto(valor):
return "".join(ch if ch.isprintable() else "?" for ch in str(valor))
Limita también el tamaño para evitar redraw costoso y memoria sin control.
Pruebas
Prueba terminal pequeño, resize continuo, ausencia de color, 8 y 256 colores, Unicode, SSH, tmux, screen, teclados distintos, mouse ausente, excepción durante dibujo y Ctrl+C.
Usa pty para integración, pero verifica manualmente en terminales reales porque terminfo y curses cambian.
Arquitectura recomendada
Separa estado, comandos y render. Lee una tecla, conviértela en acción, actualiza el modelo y redibuja desde ese modelo. No mezcles lógica de negocio con llamadas addstr().
Errores comunes
Los fallos frecuentes son omitir wrapper(), dibujar fuera de ventanas, asumir que len() es ancho visual, depender solo de color, ignorar resize, usar busy loop, mezclar ANSI, llamar curses desde varios threads y mostrar controles no confiables.
Conclusión
curses crea interfaces de terminal portables y eficientes basadas en las capacidades reales del sistema. Usa wrapper(), valida dimensiones, agrupa refresh, maneja Unicode y resize y conserva la UI en un thread.
Prueba con terminales y terminfo reales y ofrece fallback cuando curses no esté disponible. Consulta la documentación oficial de curses y ncurses(3X).







