NiceGUI frente a Streamlit: interfaces web en Python sin frontend

Python siempre ha sido cómodo para escribir scripts, automatizar tareas, analizar datos o levantar pequeños servicios. El problema aparece cuando ese script empieza a necesitar una interfaz: un formulario, una tabla, un botón para lanzar una tarea o una gráfica para enseñar resultados. Durante años, Streamlit ha sido una de las respuestas más rápidas para ese escenario, sobre todo en datos y machine learning. Pero NiceGUI está ganando interés entre desarrolladores que quieren una UI web más cercana a una aplicación tradicional sin salir de Python.

NiceGUI no pretende convertir Python en React ni sustituir a un frontend profesional cuando el producto lo exige. Su punto fuerte está en otro sitio: permite crear interfaces web con componentes declarados en Python, ejecuta la lógica en el servidor y actualiza el navegador mediante eventos. Para herramientas internas, paneles de administración, prototipos, apps de laboratorio, robótica, IoT o utilidades de datos, esa combinación puede ahorrar mucho tiempo.

Por qué NiceGUI atrae a desarrolladores Python

NiceGUI se define como un framework Python para crear interfaces que se muestran en el navegador. La documentación oficial destaca que permite construir botones, diálogos, Markdown, escenas 3D, gráficos y otros componentes desde Python, y lo orienta a microaplicaciones web, dashboards, robótica, domótica y herramientas de machine learning.

Su arquitectura explica parte del atractivo. NiceGUI se apoya en FastAPI para la capa HTTP, utiliza WebSockets para actualizaciones en tiempo real y delega la interfaz visual en Vue y Quasar. El desarrollador no tiene que escribir esa parte frontend para empezar. Define componentes en Python, recibe eventos en callbacks y puede llamar directamente a pandas, NumPy, modelos de IA, APIs internas o cualquier otra librería del ecosistema.

Un “hola mundo” es casi tan directo como cabría esperar:

from nicegui import ui

ui.label('Hola, NiceGUI')
ui.button('Púlsame', on_click=lambda: ui.notify('Botón pulsado'))

ui.run(title='Demo NiceGUI', port=8080)

Al ejecutar:

python app.py

la aplicación queda disponible en http://localhost:8080. No hay plantilla HTML, ni bundle de JavaScript, ni separación inicial entre backend y frontend.

La diferencia con Streamlit aparece en el modelo mental. Streamlit funciona muy bien cuando se quiere convertir un script de datos en una app interactiva. Su modelo se basa en reejecutar el script cuando el usuario interactúa con widgets, aunque en versiones recientes ofrece mecanismos como fragmentos para controlar mejor esa ejecución. NiceGUI, en cambio, se parece más a una app guiada por eventos: defines componentes, adjuntas callbacks y actualizas partes concretas de la interfaz.

Aspecto
Streamlit
NiceGUI
Modelo principal
Script interactivo que se reejecuta
Componentes, eventos y callbacks
Casos fuertes
Datos, demos, ML, análisis rápido
Herramientas internas, paneles, control, apps web ligeras
Control del layout
Sencillo y suficiente para dashboards
Más cercano a una UI por componentes
Lógica de servidor
Python
Python
Frontend visible para el desarrollador
Muy abstraído
Muy abstraído, pero con más composición
Curva de entrada
Muy baja
Baja, algo más estructurada

Layouts, eventos y estado con código real

NiceGUI usa context managers para definir jerarquía visual. La indentación de Python marca qué componente vive dentro de otro:

from nicegui import ui

with ui.header(elevated=True).classes('bg-blue-800 text-white'):
   ui.label('Panel interno').classes('text-xl font-bold')

with ui.row().classes('w-full gap-4 p-4'):
   with ui.card().classes('w-1/2'):
       ui.label('Estado del sistema').classes('text-lg font-semibold')
       ui.badge('OK', color='green')

   with ui.card().classes('w-1/2'):
       ui.label('Acciones').classes('text-lg font-semibold')
       ui.button('Reiniciar tarea', on_click=lambda: ui.notify('Tarea reiniciada'))

ui.run()

El uso de .classes() permite aplicar utilidades tipo Tailwind para ajustar anchura, márgenes, colores o disposición. No hace falta escribir CSS para una herramienta interna sencilla, aunque entender clases básicas ayuda a que la interfaz no parezca improvisada.

El estado puede gestionarse de varias formas. Para una demo local puede bastar un diccionario global, pero en aplicaciones multiusuario conviene usar estado por usuario o definirlo dentro de cada página para evitar que dos sesiones compartan datos sin querer:

from nicegui import ui, app

@ui.page('/')
def index():
   contador = app.storage.user.get('contador', 0)
   etiqueta = ui.label(f'Clicks: {contador}')

   def incrementar():
       app.storage.user['contador'] = app.storage.user.get('contador', 0) + 1
       etiqueta.set_text(f"Clicks: {app.storage.user['contador']}")

   ui.button('Sumar', on_click=incrementar)

ui.run(storage_secret='cambia-este-secreto')

Ese detalle es importante. Si se usa una variable global para guardar datos de una app, todos los usuarios pueden terminar viendo o modificando el mismo estado. En una herramienta interna con varios compañeros conectados, ese error aparece antes de lo que parece.

Ejemplo completo: explorador de CSV con NiceGUI

Un caso práctico para desarrolladores es crear un pequeño explorador de CSV: subir un archivo, leerlo con pandas, mostrar una tabla y dibujar una columna numérica. El siguiente ejemplo es funcional y puede servir como base para paneles internos más complejos.

Primero se instalan las dependencias:

pip install nicegui pandas

Después se crea app.py:

from nicegui import ui, events
import pandas as pd
import io

@ui.page('/')
def main():
   estado = {'df': None}

   with ui.header(elevated=True).classes('bg-slate-900 text-white'):
       ui.label('CSV Data Explorer').classes('text-xl font-bold')

   with ui.column().classes('w-full p-6 gap-4'):

       with ui.card().classes('w-full'):
           ui.label('Subir CSV').classes('text-lg font-semibold')
           upload = ui.upload(auto_upload=True).classes('w-full')

       with ui.card().classes('w-full'):
           ui.label('Vista previa').classes('text-lg font-semibold')
           tabla = ui.column().classes('w-full')

       with ui.card().classes('w-full'):
           ui.label('Visualización').classes('text-lg font-semibold')
           with ui.row().classes('items-center gap-4'):
               selector = ui.select([], label='Columna numérica').classes('w-64')
               boton = ui.button('Dibujar', icon='bar_chart')
           grafico = ui.column().classes('w-full')

   def refrescar_tabla() -> None:
       df = estado['df']
       if df is None:
           return

       tabla.clear()
       grafico.clear()

       selector.options = list(df.columns)
       selector.value = None
       selector.update()

       columnas = [
           {'name': col, 'label': col, 'field': col}
           for col in df.columns
       ]

       filas = df.head(20).fillna('').to_dict('records')

       with tabla:
           ui.table(
               columns=columnas,
               rows=filas,
               row_key=df.columns[0],
               pagination=10,
           ).classes('w-full')

   def cargar_csv(e: events.UploadEventArguments) -> None:
       try:
           contenido = e.content.read()
           df = pd.read_csv(io.BytesIO(contenido))
           estado['df'] = df
           refrescar_tabla()
           ui.notify(
               f'CSV cargado: {len(df)} filas y {len(df.columns)} columnas',
               type='positive',
           )
       except Exception as exc:
           ui.notify(f'No se pudo leer el CSV: {exc}', type='negative')

   def dibujar_columna() -> None:
       df = estado['df']
       col = selector.value

       if df is None:
           ui.notify('Primero sube un CSV', type='warning')
           return

       if not col:
           ui.notify('Selecciona una columna', type='warning')
           return

       if not pd.api.types.is_numeric_dtype(df[col]):
           ui.notify(f'La columna "{col}" no es numérica', type='warning')
           return

       serie = df[col].dropna().head(50)

       grafico.clear()
       with grafico:
           ui.echart({
               'title': {'text': f'Primeros 50 valores de {col}'},
               'tooltip': {'trigger': 'axis'},
               'xAxis': {
                   'type': 'category',
                   'data': [str(i) for i in range(len(serie))],
               },
               'yAxis': {'type': 'value'},
               'series': [{
                   'type': 'bar',
                   'data': serie.tolist(),
               }],
           }).classes('w-full h-96')

   upload.on_upload(cargar_csv)
   boton.on_click(dibujar_columna)

ui.run(title='CSV Data Explorer', port=8080, reload=False)

La app tiene cuatro comportamientos básicos: acepta un CSV válido, muestra sus primeras filas, permite elegir una columna y dibuja los primeros 50 valores si la columna es numérica. Si el usuario sube un archivo incorrecto o elige texto en lugar de números, recibe una notificación.

Este ejemplo también enseña una práctica recomendable: el estado del DataFrame vive dentro de la función de página, no en una variable global compartida por todos los usuarios. Para una app real habría que añadir límites de tamaño de archivo, validación de nombres de columnas, autenticación y quizá persistencia, pero el patrón base ya está ahí.

Asincronía: no bloquear la interfaz

NiceGUI trabaja sobre un entorno asíncrono. Si un callback ejecuta una tarea pesada, puede bloquear actualizaciones para otros usuarios. Para operaciones de entrada y salida, consultas a APIs o procesos intensivos que no sean asíncronos, es mejor delegar el trabajo:

import asyncio
from nicegui import ui

def tarea_pesada(numero: int) -> int:
   # Simula cálculo costoso
   total = 0
   for i in range(5_000_000):
       total += i % numero
   return total

async def ejecutar():
   spinner = ui.spinner(size='lg')
   try:
       resultado = await asyncio.to_thread(tarea_pesada, 7)
       ui.notify(f'Resultado: {resultado}', type='positive')
   finally:
       spinner.delete()

ui.button('Ejecutar tarea pesada', on_click=ejecutar)
ui.run()

Para scripts pequeños no siempre hará falta, pero en una herramienta usada por varias personas conviene separar la interfaz de las tareas lentas. En proyectos más serios se puede usar una cola externa, Celery, RQ, procesos separados o workers propios.

Despliegue y límites reales

NiceGUI se despliega como una aplicación Python web. En local basta con ui.run(), pero en producción hay que pensar como en cualquier servicio: proxy inverso, HTTPS, autenticación, logs, permisos de subida, límites de memoria y control de errores. También conviene decidir si la herramienta será una utilidad interna en una VPN, una app detrás de autenticación corporativa o un servicio expuesto a internet.

NiceGUI tiene sentido cuando el equipo quiere moverse rápido sin crear dos proyectos separados, backend y frontend, desde el primer día. Pero no es la mejor elección si la aplicación necesita una experiencia visual muy personalizada, un equipo frontend dedicado, SEO público, rutas complejas de producto o una arquitectura SPA avanzada. En esos casos, FastAPI más React, Vue o Svelte puede ser una decisión más sostenible.

Para desarrolladores Python, la lectura es práctica: Streamlit sigue siendo excelente para análisis, demos y aplicaciones de datos rápidas. NiceGUI merece la pena cuando se necesita una interfaz más estructurada, con eventos, componentes persistentes y comportamiento más parecido a una aplicación web interna. No elimina la necesidad de diseñar bien, pero reduce mucho la distancia entre “tengo un script útil” y “mi equipo puede usarlo desde el navegador”.

Preguntas frecuentes

¿NiceGUI sirve para crear aplicaciones reales o solo demos?
Puede servir para aplicaciones internas reales, paneles de administración, dashboards y herramientas operativas. En producción hay que añadir autenticación, proxy inverso, HTTPS, límites de subida y control de recursos.

¿Es mejor NiceGUI que Streamlit?
Depende del caso. Streamlit suele ser más directo para análisis de datos y demos rápidas. NiceGUI encaja mejor cuando se busca una UI por componentes, callbacks y eventos más parecida a una aplicación web.

¿Puedo usar pandas, NumPy o modelos de IA dentro de NiceGUI?
Sí. Los callbacks se ejecutan en Python, así que pueden llamar a librerías del ecosistema como pandas, NumPy, scikit-learn, PyTorch, OpenAI SDK o APIs internas.

¿Hace falta saber JavaScript para usar NiceGUI?
No para empezar. La interfaz se escribe en Python. Conocer HTML, CSS o conceptos web ayuda cuando se quiere personalizar más el diseño o preparar despliegues complejos.

Más información: NiceGUI

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP