Pyodide
es un proyecto de código abierto salido de las tripas de Mozilla y liberado bajo la licencia Mozilla Public License 2.0.
Permite ejecutar código Python en el navegador.
Pero, ¿cómo?
Compilando CPython a WebAssembly. Pero, además de haber hecho eso con CPython también lo han hecho con toda la biblioteca estándar y con paquetes del stack científico como numpy
, pandas
, matplotlib
, scipy
, sympy
, scikit-learn
, statsmodels
, astropy
,…
¿Qué tiene que ver eso con Pybonacci?
Pues que gracias a Pyodide
ahora puedes utilizar Python desde Pybonacci sin necesidad de instalar nada. Esto puede ser interesante cuando estás en el móvil, en un PC sin lo que necesitas instalado, etc. En la barra de menús de arriba de la página ahora verás una opción que se llama ‘RunPy’ desde la que podrás acceder a Python en tu navegador.
¿Cómo se ha metido en Pybonacci (y cómo lo puedes meter tú en tu página?
En lo que figura a continuación voy a describir una serie de cosas que he tenido que hacerle a Pyodide
para que sea más usable en el navegador.
Layout de la página
El layout dispone de dos columnas, en la parte izquierda se puede ver:
- un botón para ejecutar el código (también se puede ejecutar usando “Shift+Enter”, como en una celda de Jupyter Notebook).
- Un área que es donde podemos escribir código. Para escribir código se usa
codemirror
para tener un editor más interesante. - Un área donde se puede ver el resultado de los
print
s, etc.
En la parte derecha se ve, inicialmente, una zona en blanco pero es la zona donde se pintarán los gráficos de matplotlib
si decides hacer gráficos.


El código del layout se reduce basicamente a esto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<style> .CodeMirror { font-family: Arial, monospace; font-size: 16px; } * { box-sizing: border-box; } .ecolumn { float: left; width: 50%; padding: 10px; height: 100%; /* Should be removed. Only for demonstration */ } /* Clear floats after the columns */ .erow:after { content: ""; display: table; clear: both; } @media screen and (max-width: 600px) { .ecolumn { width: 100%; } } </style> <!--HTML LAYOUT--> <p>You can execute any Python code. Just enter something in the box below and click the button.</p> <div class="erow"> <div id="left" class="ecolumn"> <button onclick='evaluatePython()'>Run</button> (or push Shift+Enter to execute the code) <br> <br> <textarea id='editorarea'></textarea> <textarea id='output' style='width: 100%;' rows='6' disabled></textarea> </div> <div id="right" class="ecolumn"> <div id="plotarea"></div> </div> </div> |
Funcionalidad de la página, librerías de terceros
Para que la página funcione hace falta cargar una serie de bibliotecas Javascript (codemirror
, pyodide
).
AVISO sobre privacidad: En este momento, las librerías de terceros se cargan desde un CDN (jsdeliver) por lo que además de a Pybonacci estaréis haciendo peticiones a otra página.
1 2 3 4 5 6 7 8 |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.58.2/lib/codemirror.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.58.2/theme/cobalt.css"> <script src="https://cdn.jsdelivr.net/npm/codemirror@5.58.2/lib/codemirror.js"></script> <script src="https://cdn.jsdelivr.net/npm/codemirror@5.58.2/mode/python/python.js"></script> <script type="text/javascript"> window.languagePluginUrl = 'https://cdn.jsdelivr.net/pyodide/v0.15.0/full/'; </script> <script src="https://cdn.jsdelivr.net/pyodide/v0.15.0/full/pyodide.js"></script> |
Funcionalidad de la página, javascript local para añadir funcionalidad
El resto del código que uso es un script en Javascript que personaliza un poco como se usa Pyodide
desde Pybonacci. Pongo el código completo del script a continuación y lo comento posteriormente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
<script> const editorarea = document.getElementById("editorarea"); const output = document.getElementById("output"); // Codemirror instance var editor = CodeMirror.fromTextArea( editorarea, { value: '# Start writing python code', lineNumbers: true, matchBrackets: true, theme: 'cobalt', } ); editor.setSize('100%', 300); // function to add output to their html textarea function addToOutput(s) { output.value += s + '\n'; } // function to add a shortcut to easily run the code in the editor // shortcut: Shift + Enter function run_shortcut(e) { if (e.shiftKey && e.keyCode == 13) { e.preventDefault(); evaluatePython(); } }; document.addEventListener('keydown', run_shortcut, false); // Initialization of PyOdide output.value = 'Initializing...\n'; languagePluginLoader.then(() => { // Injection of some names like a `print` that will print in the output // textarea. By default, PyOdide `print` prints in the brower js console... // The default PyOdide `print is renamed to `_print` // Injection of a context manager to plot figures on the right of the // page. It wraps the figure and helps to plot in the correct area using // and img html tag. var prev = "import sys\n"; prev += "import io\n"; prev += "import base64\n"; prev += "import matplotlib.pyplot as plt\n"; prev += "import js\n"; prev += "\n"; prev += "_print = print\n"; prev += "def print(msg):\n"; prev += " output = js.document.getElementById('output')\n"; prev += " output.value += str(msg) + '\\n'"; prev += "\n"; prev += "class Plot:\n"; prev += " def __init__(self, nrows=1, ncols=1):\n"; prev += " self.nrows = nrows\n"; prev += " self.ncols = ncols\n"; prev += " def __enter__(self):\n"; prev += " fig, axes = plt.subplots(ncols=self. ncols, nrows=self.nrows)\n"; prev += " self.fig = fig\n"; prev += " self.axes = axes\n"; prev += " return self.fig, self.axes\n"; prev += " def __exit__(self, exc_type, exc_value, exc_traceback):\n"; prev += " img = io.StringIO()\n"; prev += " plt.savefig(img, format='svg')\n"; prev += " img.seek(0)\n"; prev += " imgdata = img.read()\n"; prev += " plotarea = js.document.getElementById('plotarea')\n"; prev += " imgtag = js.document.createElement('img')\n"; prev += " imgtag.setAttribute(\n"; prev += " 'src','data:image/svg+xml;base64,' + \n"; prev += " str(base64.b64encode(imgdata.encode()),\n"; prev += " 'ascii')\n"; prev += " )\n"; prev += " plotarea.appendChild(imgtag)"; pyodide.runPythonAsync(prev) .then(output => addToOutput(output)) .catch((err) => { addToOutput(err) }); output.value += 'Ready!\n'; }); // Function used to evaluate the code in the editor function evaluatePython() { output.value = ''; document.getElementById("plotarea").innerHTML = ""; pyodide.runPythonAsync(editor.getValue()) .then(output => addToOutput(output)) .catch((err) => { addToOutput(err) }); } </script> |
El código anterior lo podemos desgajar en varias partes. En la primera parte accedo a varios elementos del DOM que usaré más tarde e instancio la clase CodeMirror
para poder configurar un poco el editor de código que se usa. Indico que quiero que aparezca el número de línea, que el tema del editor sea cobalt
(oscuro),…:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const editorarea = document.getElementById("editorarea"); const output = document.getElementById("output"); // Codemirror instance var editor = CodeMirror.fromTextArea( editorarea, { value: '# Start writing python code', lineNumbers: true, matchBrackets: true, theme: 'cobalt', } ); editor.setSize('100%', 300); |
La función addToOutput
lo único que hace es añadir información en la textarea
que hay debajo del editor, que es la zona donde sale la información de un print
, por ejemplo:
1 2 3 4 |
// function to add output to their html textarea function addToOutput(s) { output.value += s + '\n'; } |
La función run_shortcut
añade la funcionalidad necesaria para que al pulsar ‘Shift+Enter’ se lea el código que contiene el editor y se mande a Pyodide
para que lo ejecute (con ayuda de la función evaluatePython
):
1 2 3 4 5 6 7 8 9 |
// function to add a shortcut to easily run the code in the editor // shortcut: Shift + Enter function run_shortcut(e) { if (e.shiftKey && e.keyCode == 13) { e.preventDefault(); evaluatePython(); } }; document.addEventListener('keydown', run_shortcut, false); |
Lo siguiente tiene un poco más de miga. En esta parte se inicializa el editor. Cada vez que se inicializa se inyecta algo de código Python que evalúa Pyodide
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// Initialization of PyOdide output.value = 'Initializing...\n'; languagePluginLoader.then(() => { // Injection of some names like a `print` that will print in the output // textarea. By default, PyOdide `print` prints in the brower js console... // The default PyOdide `print is renamed to `_print` // Injection of a context manager to plot figures on the right of the // page. It wraps the figure and helps to plot in the correct area using // and img html tag. var prev = "import sys\n"; prev += "import io\n"; prev += "import base64\n"; prev += "import matplotlib.pyplot as plt\n"; prev += "import js\n"; prev += "\n"; prev += "_print = print\n"; prev += "def print(msg):\n"; prev += " output = js.document.getElementById('output')\n"; prev += " output.value += str(msg) + '\\n'"; prev += "\n"; prev += "class Plot:\n"; prev += " def __init__(self, nrows=1, ncols=1):\n"; prev += " self.nrows = nrows\n"; prev += " self.ncols = ncols\n"; prev += " def __enter__(self):\n"; prev += " fig, axes = plt.subplots(ncols=self. ncols, nrows=self.nrows)\n"; prev += " self.fig = fig\n"; prev += " self.axes = axes\n"; prev += " return self.fig, self.axes\n"; prev += " def __exit__(self, exc_type, exc_value, exc_traceback):\n"; prev += " img = io.StringIO()\n"; prev += " plt.savefig(img, format='svg')\n"; prev += " img.seek(0)\n"; prev += " imgdata = img.read()\n"; prev += " plotarea = js.document.getElementById('plotarea')\n"; prev += " imgtag = js.document.createElement('img')\n"; prev += " imgtag.setAttribute(\n"; prev += " 'src','data:image/svg+xml;base64,' + \n"; prev += " str(base64.b64encode(imgdata.encode()),\n"; prev += " 'ascii')\n"; prev += " )\n"; prev += " plotarea.appendChild(imgtag)"; pyodide.runPythonAsync(prev) .then(output => addToOutput(output)) .catch((err) => { addToOutput(err) }); output.value += 'Ready!\n'; }); |
¿Qué se inyecta y por qué?
- Por defecto, la función
print
que todos conocemos de Python enPyodide
se muestra en la consola Javascript del navegador. Es decir, para ver la información delprint
tienes que abrir las herramientas del desarrollador de tu navegador e ir a la consola Javascript para ver lo que está mostrando Python. Como esto es un poco engorroso lo he modificado un poco para poder usar algo mínimamente más conveniente. Cuando se iniciaPyodide
en Pybonacci sobreescribo la funciónprint
y elprint
original de Python se renombra como_print
. Por tanto, elprint
que te encontrarás en el editor es una versión muy básica deprint
que te permite mostrar cosas sencillas en la zona detextarea
debajo del editor. Si quieres usar elprint
completo de Python lo tendrás que llamar usando_print
y el resultado se mostrará en la consola Javascript de tu navegador. Todo esto lo hago inyectando este código al inicio:
1 2 3 4 5 6 7 8 9 10 11 |
import sys import io import base64 import js _print = print def print(msg): output = js.document.getElementById('output') output.value += str(msg) + '\n' |
- Por otra parte, al usar
matplotlib
desdePyodide
, cuando hacemos, por ejemplo, unplt.show
no sabe dónde debe dibujar las cosas. En el escritorio disponemos de una serie de backends gráficos (Qt, Tk, etc). En el navegador no mostrará nada y no se verá la figura. Para facilitar un poco el poder usarmatplotlib
enPyodide
desde Pybonacci he inyectado algo más de código. El código es un context manager que envuelve un poco el resultado de la gráfica y lo mete en una etiqueta htmlimg
y se añade al DOM de la página. El código es el siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import sys import io import base64 import matplotlib.pyplot as plt import js class Plot: def __init__(self, nrows=1, ncols=1): self.nrows = nrows self.ncols = ncols def __enter__(self): fig, axes = plt.subplots(ncols=self. ncols, nrows=self.nrows) self.fig = fig self.axes = axes return self.fig, self.axes def __exit__(self, exc_type, exc_value, exc_traceback): img = io.StringIO() plt.savefig(img, format='svg') img.seek(0) imgdata = img.read() plotarea = js.document.getElementById('plotarea') imgtag = js.document.createElement('img') imgtag.setAttribute( 'src','data:image/svg+xml;base64,' + str(base64.b64encode(imgdata.encode()), 'ascii') ) plotarea.appendChild(imgtag) |
El anterior context manager me permite crear las Figure
s y Axes
de matplotlib
y que el resultado se vea posteriormente en la página. Se puede hacer así:
1 2 |
with Plot() as (fig, ax): ax.plot((1,2,3)) |
Si quieres meter varios Axes
en la misma Figure
podrías hacer:
1 2 3 |
with Plot(1, 2) as (fig, axs): axs[0].plot((1,2,3)) axs[1].plot((3,2,1)) |
La última parte de la inicialización incluye el ejecutar el código que he inyectado mediante Pyodide
para que print
, _print
y el context manager Plot
que he creado estén disponibles de primeras en el editor sin necesidad de que hagas nada por tu parte.
Por último, para que se ejecute el código del editor después de que pulses el botón ‘Run’ o de que pulses las teclas ‘Shift+Enter’ se usa la función evaluatePython
:
1 2 3 4 5 6 7 8 |
// Function used to evaluate the code in the editor function evaluatePython() { output.value = ''; document.getElementById("plotarea").innerHTML = ""; pyodide.runPythonAsync(editor.getValue()) .then(output => addToOutput(output)) .catch((err) => { addToOutput(err) }); } |
La función anterior, primero limpia la zona del output
(donde se muestra el resultado de un print
). Limpia también la zona donde se muestran los gráficos para que se añadan los nuevos, si los hubiera en el nuevo código que estamos evaluando, y ejecuta el código del editor.
Mejoras y errores
Si quieres ver mejoras o algo no funciona bien puedes comentarlo en este artículo o puedes ir al repositorio que he creado para esto:
Hola,
¿crees que se podría utilizar pyodide para correr algún add-on en Firefox?
Estuve mirando y aparentemente todos los add-on están escritos en Java:(
Gracias anticipadas,
No sé lo que quieres hacer pero la mejor opción que tienes es usar directamente Javascript (que no java) y otras tecnologías web. Este puede ser un buen punto de partida: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension
Potencialmente podrías usar Brython para escribir tu addon con Python: https://brython.info/
Con Pyodide nunca me lo he planteado, la verdad, pero no veo porqué no podría funcionar también.
Muchas gracias por tu comentario Kiko!
Lo que estoy intentando hacer es un add-on para extraer información de páginas webs de noticias (artículo, título, fecha y autor) para descargarlo en un word, según tu tutorial de docx;)
No necesitas un addon en un navegador para eso. Cosas como beautifulsoup, lxml, requests, requests-html, gazpacho,…, te resultarán más sencillas para lo que quieres hacer.
Gracias de nuevo por el comentario Kiko,
pensaba en un add-on por su sencillez de uso (pulsas un botón y se ejecuta) frente a beautifulsoup con python en el que tendría que abrir el programa, pegar la url, etc..
Que sea sencillo de usar no implica que no sea complejo por dentro. Apretar un botón es fácil, que haga todo lo que deseas no tiene porqué ser así… Lo que quieres hacer no es trivial y te va a tocar aprender cosas.
Muchas gracias de nuevo por tu comentario Kiko.