El irónico estado de la gestión de dependencias en Python (III)
◾ En entregas anteriores de esta serie hablamos de los entornos e intérpretes y cómo ajustarlos a cada necesidad y de los mundos virtuales en Python, aciertos y errores. En esta tercera y última parte hablemos del “tercer dragón”: las dependencias.
El tercer dragón
Cuando hablamos de instalar dependencias y que unos proyectos se pisaban con otros lo solucionamos con los entornos virtuales. Eso borra de un plumazo cualquier altercado entre vecinos. Pero ¿Qué ocurre si dentro de las dependencias, dos paquetes poseen dependencias solapadas e incompatibles entre sí? Pues ese es nuestro tercer problema.
Como muchos sabéis, Python posee un repositorio central del que los proyectos se nutren para instalar las dependencias. Habitualmente, esto se gestiona con la herramienta de línea de comandos 'pip'.
El problema con pip, es que no vigila que un paquete pise las dependencias a nivel de versiones de otro. Vamos a ilustrarlo. Si tengo una librería denominada tech.py
que requiere la versión 2.0 del paquete o librería Pydantic y tengo una dependencia que usa Pydantic pero en su versión 1.2, sigo teniendo el mismo problema que tenía antes con los proyectos que usan un mismo intérprete.
La diferencia es que el problema lo tengo entre paquetes de un mismo proyecto y antes la tenía entre proyectos.
Además, pip no gestionará ese problema de resolución de dependencias sin ayuda del administrador del proyecto. De hecho, si persistimos en nuestra instalación y forzamos la inclusión de paquetes que no son compatibles dará error en algún momento de su ejecución, puesto que la funcionalidad esperada por alguno de los paquetes no se encontrará.
Bienvenido al mundo de los gestores de paquetes y dependencias para Python
No solo nos van a preocupar las dependencias cuando estemos desarrollando, testeando o desplegando la aplicación. A lo largo de la vida de ésta, querremos mantener los paquetes libres de vulnerabilidades y eso pasa por aplicar una política severa de seguridad que nos lleve a comprobar que están libres de errores, cuándo se publican nuevas actualizaciones y actualizar nuestras dependencias en caso afirmativo.
Y, ojo, porque resolver el grafo de conflictos en las dependencias es un problema bastante gordo que nos podría arrojar al abismo de la complejidad computacional dado que puede llegar a ser un problema dentro de la categoría NP-Hard.
Ya hemos puesto anteriormente ejemplos de conflictos en dependencias, pero dentro de un proyecto este problema puede agudizarse dado que unos paquetes dependerán de una versión concreta mientras que otros excluyen el uso de esa versión en particular.
El conflicto estará asegurado y muchas veces la solución pasa por ajustar el proyecto para hacer caer la balanza de un lado u otro, sin espacio posible para decisiones salomónicas.
Bien, descrito el problema y descartado pip para la gestión de dependencias de un proyecto medianamente serio (para proyectos pequeños, pip a secas puede bastar) necesitamos una herramienta que vigile los conflictos, actualice los paquetes sin problemas de compatibilidad (esto es, por ejemplo, usar semver y respetar las especificación de versiones) e incluso nos construya y publique nuestros propios paquetes Python.
El problema no es que no existan este tipo de herramientas, sino precisamente todo lo contrario. Hay tal abundancia de gestores de dependencias que cuesta comprometerse con una sola. Similar a cuando dispones de tiempo libre para ver una serie y lo consumes navegando por todo el catálogo sin decidirte a "darle al play".
Es broma, no exageremos. Si que hay varios gestores y cuesta decidirse por uno puesto que todos son competentes haciendo su trabajo y las características prácticamente se solapan. Al final, la decisión cae más por el uso y acomodo según vamos probándolos que un riguroso examen y test técnico.
Un práctico ejemplo gráfico
Vamos a presentar a tres de ellos, con mucho en común pero con distinto sabor y formas de hacer las cosas: Poetry, PDM y Rye.
Dado que poseen elementos muy comunes veremos ejemplos en Poetry que son fácilmente adaptables a los otros gestores.
Este tipo de herramientas son más que un gestor de paquetes y dependencias. Poetry, por ejemplo, nos permite desde construir el proyecto y empaquetarlo hasta publicarlo en PyPI (el repositorio por excelencia en Python).
Para la resolución de dependencias se apoya mucho en los metadatos de los paquetes publicados y accesibles en PyPI siguiendo el esquema de versionado normalizado en la pip-440 y no el semver que ya hemos comentado anteriormente.
Una cosa interesante es que cuando iniciamos un nuevo proyecto nos crea una estructura de directorios y archivos típicos en un proyecto Python, ahorrándonos cierto trabajo de entrada. Un ejemplo:

De un plumazo, el comando "new", nos ha puesto el directorio para las fuentes (que opcionalmente también puede ser src
) y uno para animarnos a escribir los siempre necesarios pero víctimas de la procrastinación, tests.
Vamos a instalar una dependencia, por ejemplo el paquete request
:

Como vemos, nos instala el paquete, sus dependencias y lo mejor es que en vez de usar el requirements.txt
de toda la vida (podemos exportar a ese formato) nos lo declara en un archivo pyproject.toml que ya está definido en los estándares Python.
Observemos el contenido de este archivo:

Si observamos bien, no están detalladas las dependencias. Esto es adrede. Lo que tenemos es una vista "clara" de lo que el proyecto necesita, pero nos abstraemos de las dependencias de cada paquete.
Esas dependencias de terceros están definidas en un archivo "lock" que ya hemos visto pero replicamos observar cómo queda tras la instalación de una librería necesaria:

Lo que vemos es el final del archivo, ya que es denso y difícilmente procesable en una lectura a vuelapluma. Es más un registro para el gestor de paquetes que le sirve para obtener una foto fija de versiones "que funcionan".
Luego, cuando queramos replicar el entorno de ejecución en un despliegue, no habrá (o no debería) conflictos innecesarios porque es un ambiente prácticamente clonado.
PDM y Rye son similares en cuanto funcionamiento, aunque huelga decir que Rye opta por un camino distinto y posee incluso la funcionalidad de pyenv
, es decir, posee su propio gestor de "Pythons" y nos instalará la versión deseada (si está disponible) y la acoplará al proyecto.
El caso de Rye es curioso porque está diseñado e implementado por el conocido Armin Ronacher "Mitsuhiko". Este desarrollador es el líder de proyectos tales como Flask, Click y otras librerías ampliamente usadas.
Armin ha fusionado las necesidades de gestión de paquetes Python pero desde la perspectiva de Rust, dado que desde hace algunos años, Ronacher es adepto a este lenguaje del que ya hemos hablado en más de una ocasión desde aquí.
Bonus track
Cuando un gestor de dependencias "clava" los paquetes que requiere un proyecto, los hashes de estos se mantienen en un archivo (habitualmente, con la extensión .lock
) donde podemos observar la lista de paquetes, dependencias de estos y todos los hash según la versión instalada.
Esto permite comparar el hash del paquete que se está instalando y si no coincide con el anotado en el archivo el gestor notificará ese desacuerdo.

La protección es obvia: El paquete puede haber sido alterado, tanto accidentalmente o como parte de un ataque a la cadena de suministro. Ojo, que no son firmas criptográficas. Aun así, algo se tiene para asegurar que el paquete que llega no viene con el lazo flojo.
Conclusión
Poseer un ambiente de ejecución portable (en el sentido de moverlo de máquina) en Python no debe ser una penitencia a Cvstodia. Claro que hay que tropezar con todas las piedras que tiene el camino para ir acumulando experiencia.
Para un lenguaje cuyo lema es "There should be one —and preferably only one— obvious way to do it." resulta irónico que se disponga, de manera tan accidental, de tantas y tantas opciones para llegar al mismo punto.
No existe un forma ortodoxa ni tratamos de sentar cátedra aquí, pero es práctica y como se suele decir: "battle tested". La hemos usado en varios de los proyectos que lideramos desde el área de Innovación y Laboratorio y de momento nadie ha roto el build ;)
—MÁS PARTES DE ESTA SERIE