El irónico estado de la gestión de dependencias en Python (I)

27 de noviembre de 2023

Es indudable que si hay un lenguaje de programación que se ha ganado el respeto y admiración en la última década, (entre otros), ese sería Python.

¿Inteligencia artificial? Python. ¿Aplicaciones web? Python. ¿Aprendiendo a programar? Python. ¿Aprender Python? Python. Pocas tareas se le escapan a este lenguaje dinámico que comenzó desplazando al rey del scripting en los 90, Perl.

Creado por el neerlandés Guido Van Rossum a finales de los 80, Python vio su primera versión en el año 91. Desde entonces ha ido ganando posiciones en los diversos rankings anuales en los que posicionan los lenguajes de programación en varios criterios.

Cabe preguntarse si hay algo que no haga bien este lenguaje y la respuesta sería: por supuesto. Una de ellas, bastante repetida en la comunidad, es el sistema de gestión de paquetes, dependencias y versiones del intérprete cuando te metes en faena con un proyecto.

Hasta el genial creador del comic XKCD, Randall Munroe, le dedicó una de sus viñetas a este problema:


"En mi ordenador funcionaba"

A los ingenieros nos gustan los sistemas reproducibles sin trabas. Esto es: diseñas, desarrollas, pruebas y cuando todo esté listo, la teoría dice que deberías desplegar y echarlo a andar sin problemas. La práctica, sin embargo, nos enseña que el resultado es todo lo contrario.

Por ello, una de las finalidades de los contenedores o la virtualización es tratar de borrar la línea que separa los dos universos en constante discrepancia: el entorno de desarrollo y el de producción.

Respecto al ecosistema Python, es multiplataforma en el sentido de que el intérprete Python está disponible para múltiples sistemas y también te abstraes (con idénticas limitaciones a Java) de la plataforma subyacente.

Todo bien hasta ahí, salvo que en un sistema operativo puede que la versión de Python instalada no sea la misma en la que se ha desarrollado la aplicación. Es más, hace unos años el mundo Python se dividía en dos, como un cisma, con el advenimiento de la versión 3 del lenguaje:

  • Por un lado estaban quienes se resistían a cambiar desde el popular entonces Python 2.7.
  • En la otra orilla el grupo de aventureros que se internaban en el entonces yermo que era Python 3 (también conocido como Python 3000); decimos yermo porque el ecosistema de librerías aún era en extremo incipiente en cuanto a versiones para Python 3.

Por lo tanto, ya de entrada nos encontramos con el inconveniente de que el proyecto que estemos desarrollando esté atado a una versión en particular y cualquier otro sistema objetivo no posea el intérprete adecuado.

¿Cómo lo solucionamos?

Obviamente, tenemos que instalar un nuevo intérprete con la versión requerida y hacerlo convivir con el original. Existen proyectos que nos permiten hacer exactamente eso, instalar cuantos intérpretes Python queramos con un único y discreto inconveniente (que incluso puedes automatizar): tienes que hacerte cargo de seleccionar la versión adecuada antes de lanzar el proyecto o alguna función que necesites de testeo o integración continua.

Pyenv, open source y disponible para sistemas UNIX (hay un fork para Windows) es un proyecto que nos instala una utilidad de línea de comando para administrar Pythons. Es simple de usar una vez se entienden los conceptos con los que funciona.

En primer lugar te instala el intérprete que quieras. La lista es interminable. Puedes instalar desde versiones obsoletas (usadas sobre todo para test de regresión en librerías aun soportadas), interpretes alternativos (Pypy, Jython...) y distribuciones conocidas, como Anaconda.

Una vez tengamos el intérprete (o los que queramos) instalados, aún no estará activo. Aquí tenemos una diversidad de opciones para flexibilizar y adaptarlo a nuestras necesidades. En primer lugar, podemos optar por activar un intérprete a nivel global, afectando a todo el sistema. Cualquier shell nueva que creemos tendrá a ese intérprete como principal y cualquier proceso que llame a Python también tendrá a dicho intérprete como objetivo.

Por ejemplo, supongamos que quiero que el sistema operativo en general reconozca a Python 3.9.17 como el intérprete a usar. El comando es simple: pyenv global 3.9.17

Con esa orden todo "python" será el 3.9.17.

Pero claro, afectar a todo el sistema en general es algo que puede producir efectos colaterales indeseables. Lo queremos reducir a nivel de la carpeta en la que estamos trabajando. Es decir, que todas las sesiones shell que abramos en esa carpeta tengan a dicho intérprete como referencia.

Es simple, en vez de global, usaremos local. Eso nos creará un archivo denominado .python-version en cuyo interior tan solo figura el nombre con el que pyenv va a alterar las rutas del entorno para que cuando escribamos python y estemos en dicha carpeta, el intérprete sea el adecuado. Magia.

En la captura podemos verlo ilustrado. Le decimos a pyenv que en ese directorio se usará la versión 3.9.17 y toda shell que abramos ahí estará atada a dicho intérprete.

Más flexible aún. Supongamos que dentro del directorio queremos hacer una pequeña prueba con otra versión de Python. ¿Qué podemos hacer?

pyenv shell 3.11.5

Eso nos altera solo esa shell, que ahora apuntará a la versión 3.11.5 de Python. Ninguna otra sesión se verá alterada, tan solo lo que ejecutemos ahí. Observemos la captura:

Esto es Hollywood. Una vez terminemos nuestras pruebas hacemos un pyenv shell --unset y volvemos a donde estábamos.

Con esto, prácticamente solucionamos el problema de los intérpretes. Podemos tener los que queramos.

—MÁS PARTES DE ESTA SERIE

El irónico estado de la gestión de dependencias en Python (II)
El irónico estado de la gestión de dependencias en Python (III)