Artículos en profundidad sobre la tecnología que da forma al futuro.

Pyodide: Python real en el navegador con WebAssembly

Pyodide compila CPython a WebAssembly para ejecutar Python con NumPy y pandas en el navegador. Cómo funciona y cuándo tiene sentido.

Una serpiente de Python enroscada dentro de un terrario de cristal con forma de ventana de navegador y cajas de ciencia

Python en el navegador ha sido un sueño desde principios de la década de 2010, y todos los intentos han tenido el mismo problema: Python está demasiado ligado al runtime en C de CPython como para portarlo fácilmente a un entorno web. Transcrypt, Brython y Skulpt intentaron resolverlo reimplementando Python en JavaScript, lo que funcionaba con scripts sencillos pero se rompía cuando necesitabas NumPy, pandas o cualquiera de las librerías de extensión en C que hacen que Python sea realmente útil.

Pyodide sigue un enfoque distinto: compilar el intérprete real de CPython a WebAssembly. No es una reimplementación, sino el código fuente real de CPython compilado con Emscripten para ejecutarse en el navegador. Esto significa compatibilidad total con el lenguaje, incluidas las extensiones en C. Puedes hacer import numpy y funciona, porque el binario Wasm compilado incluye el código C real de NumPy.

Cómo funciona Pyodide por dentro

El proceso de compilación de Pyodide toma el árbol de código fuente de CPython y lo compila con Emscripten, una cadena de herramientas que apunta a WebAssembly en lugar de código máquina nativo. El resultado es un binario .wasm que implementa el intérprete de Python, más código de pegamento en JavaScript que lo conecta con el entorno del navegador.

La parte complicada son las extensiones en C. NumPy, por ejemplo, incluye más de 100.000 líneas de código C y Fortran. Pyodide precompila un conjunto seleccionado de paquetes populares —NumPy, pandas, scikit-learn, matplotlib, scipy— en módulos Wasm que se cargan bajo demanda. Cuando haces import numpy, Pyodide descarga el módulo Wasm de NumPy ya precompilado, lo enlaza con el intérprete en ejecución y lo inicializa.

<!-- Minimal Pyodide example -->
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.0/full/pyodide.js"></script>
<script>
async function main() {
// Load the Python interpreter (~10MB download)
const pyodide = await loadPyodide();
// Run Python code directly
pyodide.runPython(`
import sys
print(f"Python {sys.version} running in the browser!")
`);
// Load packages on demand
await pyodide.loadPackage('numpy');
// Use NumPy — the real NumPy, compiled to Wasm
const result = pyodide.runPython(`
import numpy as np
arr = np.random.randn(1000)
f"Mean: {arr.mean():.4f}, Std: {arr.std():.4f}"
`);
console.log(result);  // "Mean: 0.0123, Std: 1.0045"
}
main();
</script>

El puente entre JavaScript y Python

Una de las mayores fortalezas de Pyodide es la interoperabilidad fluida entre Python y JavaScript. Los objetos de Python se convierten automáticamente en proxies de JavaScript y viceversa. Puedes llamar funciones de JavaScript desde Python, manipular el DOM desde Python y pasar datos entre ambos lenguajes sin serialización manual.

// JavaScript calling Python
const pyodide = await loadPyodide();
// Define a Python function
pyodide.runPython(`
def analyze(data):
import statistics
return {
'mean': statistics.mean(data),
'median': statistics.median(data),
'stdev': statistics.stdev(data)
}
`);
// Call it from JavaScript with JS data
const analyze = pyodide.globals.get('analyze');
const result = analyze([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
console.log(result.toJs());  // {mean: 5.5, median: 5.5, stdev: 3.03}
// Python accessing the DOM
pyodide.runPython(`
from js import document
element = document.getElementById('output')
element.textContent = 'Updated from Python!'
`);

El sistema de proxies gestiona la conversión de tipos automáticamente: los diccionarios de Python se convierten en objetos de JavaScript, las listas en arrays y los números en números de JavaScript. Para transferencias de datos grandes, como pasar un array de NumPy de un millón de elementos a una librería de visualización en JavaScript, Pyodide usa buffers de memoria compartida para evitar copias, lo que es clave para el rendimiento.

Dónde tiene sentido Pyodide

Entornos de computación interactiva. JupyterLite, construido sobre Pyodide, ejecuta cuadernos de Jupyter completamente en el navegador. No hace falta servidor: el kernel de Python corre localmente en Wasm. Esto es transformador para la educación: el alumnado puede ejecutar cuadernos de Python sin instalar nada, y el docente no tiene que aprovisionar servidores.

Herramientas de exploración de datos. Las aplicaciones web que permiten subir datos y analizarlos pueden usar Pyodide para procesarlos íntegramente en el cliente. Ningún dato sale del navegador, lo que resuelve problemas de privacidad y elimina costes de servidor. Un cargador de CSV que aplica transformaciones con pandas en el navegador es más sencillo de desplegar y más privado que uno que envía los datos a un backend.

Documentación con ejemplos en vivo. La documentación de librerías de Python puede incluir bloques de código interactivos que realmente se ejecutan. En lugar de mostrar una salida estática, el lector puede modificar el código y ver los resultados al instante. Para aprender, esto es muchísimo más efectivo que los ejemplos estáticos.

Prototipado y experimentación. Los científicos de datos que piensan en Python pueden prototipar transformaciones y visualizaciones directamente en el navegador y luego compartir el resultado con una URL. Sin configurar entornos, sin gestionar dependencias y sin el clásico «en mi máquina funciona».

La realidad del rendimiento

Pyodide no es rápido. WebAssembly tiene una sobrecarga frente al código nativo, normalmente entre 1,5 y 3 veces más lento en trabajos intensivos en cómputo. Además, Pyodide ejecuta CPython (que ya es unas 100 veces más lento que C para código Python puro) compilado a Wasm. En bucles de Python puro, Pyodide es aproximadamente entre 2 y 5 veces más lento que CPython nativo.

Pero ojo: las cargas de trabajo donde Pyodide resulta útil suelen estar dominadas por llamadas a extensiones en C, no por Python puro. Cuando llamas a np.dot(a, b), el cálculo real ocurre en código C compilado (ahora a Wasm). La sobrecarga de Wasm sobre ese código C es de 1,5 a 3 veces, lo cual en casos de uso interactivos está perfectamente bien. Una multiplicación de matrices con NumPy que tarda 10 ms de forma nativa tarda 20 ms en Pyodide. Eso es imperceptible para un usuario que pulsa «Ejecutar» en un cuaderno.

Performance comparison (approximate):
Native CPython    Pyodide (Wasm)
Pure Python loop:     1x                3-5x slower
NumPy operations:     1x                1.5-3x slower
Pandas groupby:       1x                2-3x slower
Startup time:         50ms              2-5 seconds
Package loading:      instant (pip)     2-10s (download + init)
Startup is the real cost. Once loaded, interactive
performance is adequate for most use cases.

El coste de arranque es el problema mayor. Cargar el runtime básico de Pyodide supone descargar 10 MB. Cargar NumPy añade otros 7 MB y pandas otros 10 MB. En una página web que debe renderizarse rápido, un retraso de 5 segundos en la inicialización es un impedimento total. Por eso Pyodide funciona mejor en aplicaciones donde el usuario espera a que cargue un entorno —cuadernos, herramientas de datos, tutoriales interactivos— y no en páginas web tradicionales.

Lo que Pyodide no puede hacer

No todos los paquetes de Python funcionan en Pyodide. Los que tienen dependencias del sistema —drivers de bases de datos, librerías de interfaz gráfica, cualquier cosa que llame a APIs específicas del sistema operativo— no se compilan a Wasm. psycopg2 necesita una librería cliente de PostgreSQL y opencv necesita librerías gráficas del sistema. Estas dependencias no existen en el entorno del navegador.

La red también está limitada. El módulo socket de Python no funciona en el navegador, ya que no hay acceso a sockets crudos desde Wasm. Las peticiones HTTP pasan por la API fetch() del navegador a través del puente JavaScript de Pyodide, así que están sujetas a las restricciones de CORS. No puedes ejecutar un servidor Flask en Pyodide (no hay socket donde escuchar) ni establecer conexiones de red arbitrarias.

El multihilo tiene soporte parcial mediante Web Workers, pero el GIL (Global Interpreter Lock) de Python sigue aplicando y el modelo de hilos difiere del CPython nativo. El paralelismo limitado por CPU funciona mejor con multiprocessing (cada worker tiene su propia instancia de Wasm) que con hilos.

Pyodide frente a Python transpilado

Merece la pena comparar el enfoque de Pyodide (compilar CPython a Wasm) con la alternativa (transpilar Python a JavaScript). Proyectos como Transcrypt y Brython convierten la sintaxis de Python en JavaScript, generando código que se ejecuta de forma nativa en el motor JS del navegador.

La transpilación es más rápida en ejecución: el JavaScript generado corre a velocidad JS nativa, no a la velocidad de CPython en Wasm. Además, no tiene coste de arranque, porque no hay runtime que descargar. Pero sacrifica compatibilidad: el Python transpilado no puede ejecutar extensiones en C, tiene diferencias semánticas sutiles respecto a CPython (sobre todo en tipos numéricos, manejo de cadenas y casos límite) y soporta solo un subconjunto de la biblioteca estándar.

La ventaja de Pyodide es que ejecuta el CPython real. Si tu código Python funciona en CPython, funciona en Pyodide (salvo dependencias a nivel de sistema). Para flujos de trabajo de ciencia de datos que dependen de NumPy, pandas y scikit-learn, esa compatibilidad es innegociable. Para scripts sencillos que no necesitan extensiones en C, la transpilación puede ser la mejor opción: como en cualquier comparación entre Wasm y JavaScript nativo, la herramienta adecuada depende de la carga de trabajo.

Hacia dónde va esto

Pyodide está en desarrollo activo y mejora constantemente. Las versiones recientes han reducido el tamaño del runtime básico, han mejorado el arranque con compilación en streaming (el navegador compila Wasm mientras lo descarga) y han ampliado el conjunto de paquetes precompilados. El ecosistema de WebAssembly también madura: WASI proporciona interfaces de sistema estandarizadas y el component model permitirá una mejor interoperabilidad entre módulos Wasm.

La tendencia general es que el navegador se está convirtiendo en un runtime universal. Entre JavaScript, Wasm y proyectos como Pyodide, puedes ejecutar código escrito en casi cualquier lenguaje directamente en el navegador. Esto no reemplaza la computación en el servidor, sino que la complementa al mover al cliente el cómputo que no necesita un servidor. Para el enorme ecosistema de ciencia de datos de Python, ejecutar en el navegador abre casos de uso que antes simplemente no eran posibles.