uv: Adiós a Poetry

Hace un tiempo os hablé sobre Poetry y cómo nos podía ayudar a mejorar nuestra experiencia desarrollando en Python. Hoy vengo a hablaros de uv, una herramienta que va mucho más allá que Poetry, ¡hasta el punto de poder programar en Python sin ni siquiera tener el lenguaje instalado en nuestra máquina!

¿Qué es uv?

uv es una herramienta súper rápida para gestionar versiones de Python, entornos virtuales y dependencias en proyectos Python. Desarrollada en Rust y por los mismos creadores de ruff, uv busca reemplazar herramientas clásicas como pip, Poetry, virtualenv o pip-tools con una única solución todo en uno.

uv aprovecha al máximo la velocidad del lenguaje Rust para resolver e instalar dependencias de manera mucho más eficiente, manteniendo compatibilidad con los archivos pyproject.toml. Además, es capaz de crear y gestionar entornos virtuales de forma automática, lo que simplifica enormemente la configuración de proyectos Python desde cero.

uv está diseñado para ser una herramienta “todo en uno”, reduciendo el tiempo de espera y la complejidad del flujo de trabajo típico en Python. Ya sea que estés empezando un nuevo proyecto o manteniendo uno existente, uv promete hacer ese proceso más rápido, sencillo y confiable.

¿Cómo instalar uv?

La instalación es realmente sencilla, mi recomendación es ejecutar esto en tu terminal:

curl -LsSf https://astral.sh/uv/install.sh | sh

De forma alternativa, si eres usuario de macOS puedes usar Homebrew:

brew install astral-sh/tap/uv

Crear un proyecto con uv

Crear un proyecto con uv es realmente sencillo, basta con escribir en nuestra terminal:

uv init weather_app

Cuando la creación haya finalizado, tendremos una carpeta weather_app con la siguiente estructura:

.
├── .gitginore
├── .python-version
├── main.py
├── pyproject.toml
└── README.md

Como podemos ver, uv nos ha creado los siguientes ficheros:

  • .gitignore: Archivos que Git debe ignorar.
  • .python-version: El archivo .python-version contiene la versión de Python predeterminada del proyecto. Este archivo le indica a uv qué versión de Python usar al crear el entorno virtual del proyecto.
  • main.py: Punto de entrada de la aplicación.
  • pyproject.toml: El archivo pyproject.toml contiene metadatos sobre tu proyecto. Usarás este archivo para especificar dependencias, así como detalles del proyecto como su descripción o licencia. Puedes editar este archivo manualmente o usando comandos de uv.
  • README.md: Documento con la descripción de la aplicación.

Añadir uv a un proyecto existente

De igual forma, es posible añadir uv a un proyecto existente. Para ello, simplemente debemos ejecutar en nuestra terminal uv init dentro de la carpeta del proyecto en cuestión.

En este caso simplemente nos creará los ficheros .python-version y pyproject.toml dentro de la carpeta de nuestro proyecto.

Migrar un proyecto a uv

Una característica que me gusta mucho de uv es la herramienta uvx, que entre otras muchas cosas nos permite migrar nuestros proyectos a uv.

En mi caso, es algo que he tenido que hacer en mi trabajo, ya que previamente estábamos usando Poetry y, si no llega a ser por esta herramienta, hubiera sido súper tedioso.

Para la migración, solo necesitamos ejecutar el siguiente comando dentro de la carpeta del proyecto a migrar y uvx se encargará del resto (básicamente, adaptar el fichero pyproject.toml a uv, borrar el poetry.lock y crear un nuevo uv.lock):

uvx migrate-to-uv

Cómo usar uv

uv tiene multitud de comandos (se pueden listar todos con el comando uv help), pero yo me quiero centrar en los que creo que son más importantes:

  • uv python install x.y.z: nos permite instalar una versión de Python concreta para nuestro entorno virtual específico de esta aplicación.
  • uv python pin x.y.z: nos permite fijar una versión de Python concreta para nuestro entorno virtual específico de esta aplicación.
  • uv sync: nos permite instalar nuestras dependencias.
  • uv lock --upgrade: nos permite actualizar nuestras dependencias.
  • uv add PACKAGE_NAME: nos permite instalar nuevos paquetes.
  • uv run COMMAND: nos permite ejecutar comandos dentro de nuestro entorno virtual:
    • uv run pytest tests/ -ra
    • uv run mypy .
    • uv run ruff format src tests

Usar Make para operar con uv

Aunque es muy interesante conocer cómo operar con uv, creo que tiene más valor centrarse en otras cosas y no tener que estar recordando cómo eran los comandos del apartado anterior. Por ello, yo soy muy fan de usar Makefile para darle más semántica a estos comandos y, por tanto, que sean más fáciles de recordar.

Esta podría ser la forma de nuestro Makefile para operar uv:

.DEFAULT_GOAL := help 

.PHONY: help
help:  ## Show this help.
	@grep -E '^\S+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | \
		awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}'

pre-requirements:
	@scripts/pre-requirements.sh

.PHONY: install
install: pre-requirements ## Install the app packages
	uv python install 3.12.8
	uv python pin 3.12.8
	uv sync

.PHONY: update
update: pre-requirements ## Updates the app packages
	uv lock --upgrade

.PHONY: add-package
add-package: pre-requirements ## Installs a new package in the app. ex: make install package=XXX
	uv add $(package)

.PHONY: run
run: pre-requirements ## Runs the app in production mode
	uv run python main.py

.PHONY: check-typing
check-typing: pre-requirements  ## Run a static analyzer over the code to find issues
	uv run mypy .

.PHONY: check-lint
check-lint: pre-requirements ## Checks the code style
	uv run ruff check

.PHONY: lint
lint: pre-requirements ## Lints the code format
	uv run ruff check --fix

.PHONY: check-format
check-format: pre-requirements  ## Check format python code
	uv run ruff format --check

.PHONY: format
format: pre-requirements  ## Format python code
	uv run ruff format

.PHONY: checks
checks: check-lint check-format check-typing  ## Run all checks

.PHONY: test
test: pre-requirements ## Run tests
	uv run pytest -n auto tests -ra

Con este Makefile creado, tendremos a nuestra disposición una serie de comandos tales como:

  • make install
  • make update
  • make add-package package=pytest
  • make run
  • make test
  • make format
  • make check-typing

Un detalle menor, pero no por ello menos importante, es que en todos los comandos de Make podemos ver que se hace referencia a un comando llamado pre-requirements. Este comando no es ni más ni menos que un script, el cual valida que tenemos uv instalado en nuestra máquina, ya que es lo único que necesitamos para poder trabajar en nuestro proyecto.

El script en cuestión tiene este aspecto:

#!/bin/bash

function check_uv() {
    if ! command -v uv &> /dev/null; then
        echo "uv is not installed. Please go to https://docs.astral.sh/uv/getting-started/installation/."
        return 1
    fi
    return 0
}

check_uv

Configurar uv

Toda la descripción y configuración de uv reside en el fichero pyproject.toml. En él vamos a poder definir todo lo relativo a nuestro propio proyecto, así como a las librerías externas que usemos, como ruff o mypy.

[project]
name = "weather-app"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

...

[tool.pyright]
venvPath = "."
venv = ".venv"

[tool.ruff]
line-length = 120

[tool.mypy]
check_untyped_defs = true

...

En este ejemplo que os muestro hemos modificado:

  • El tamaño de la línea para ruff (nuestro formateador).
  • La localización de nuestro entorno virtual para pyright (nuestro LSP para Python).
  • La configuración de mypy respecto al tipado.

Activado y desactivado automático de los entornos virtuales

Una cosa muy importante, aunque no estrictamente necesaria, es el manejo de los entornos virtuales creados por uv. Será necesaria la activación de los mismos cuando estemos dentro de la carpeta del proyecto y su desactivación cuando salgamos.

Aunque esto se puede hacer de forma manual con un par de comandos:

  • source .venv/bin/activate: Para activar el entorno virtual.
  • deactivate: Para desactivar el entorno virtual.

Existen alternativas para que esta activación y desactivación sea automática. Si usas oh-my-zsh, como es mi caso, puedes instalar el plugin pyautoenv, el cual se encargará de todo y tú te podrás olvidar de los entornos virtuales.

=> cd weather_app
(weather_app) => git:(main)
(weather_app) => git:(main) cd ..
=>

A partir del momento en el que esté instalado el plugin, podremos ver, sin necesidad de activarlo, nuestro entorno virtual entre () en el prompt de nuestra terminal cuando entremos en una carpeta con un entorno virtual. De igual forma, cuando nos movemos a una carpeta sin entorno virtual, este se desactivará automáticamente.

Dockerizar una app con uv

Podemos ir un paso más allá y no tener que instalar nada en nuestra máquina (ni siquiera uv) haciendo uso de Docker.

De esta forma, podemos dockerizar nuestra app creando un fichero Dockerfile, usando la imagen de Python con la versión concreta, instalar uv y dejar todas nuestras dependencias encapsuladas dentro de nuestra imagen:

FROM python:3.12.8-alpine

RUN apk update --no-cache && apk upgrade --no-cache --available

RUN pip install --no-cache-dir uv

RUN uv python pin 3.12.8

WORKDIR /code

COPY pyproject.toml /code/pyproject.toml

RUN uv sync --no-group test

COPY main.py /code/main.py

COPY src /code/src

CMD ["uv", "run", "python", "main.py"]

Usar uv en un CI/CD (ej: GitHub Actions)

De forma muy similar a como dockerizamos nuestra app, podemos generar una pipeline con nuestro CI/CD de confianza. Por simplicidad, voy a usar GitHub Actions.

Os dejo por aquí un ejemplo muy sencillo, sacado de este repositorio:

name: Pass checks and tests
on: [push, pull_request]
permissions:
  contents: read
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Set up Python 3.12.8
      uses: actions/setup-python@v3
      with:
        python-version: "3.12.8"
    - name: Install uv
      uses: astral-sh/setup-uv@v5
    - name: Install dependencies
      run: make install
    - name: Check types, format and styles
      run: make checks
    - name: Tests
      run: make test

Conclusiones

Como hemos podido ver, uv va un paso más allá que Poetry, en tanto que no solo nos permite gestionar nuestros entornos virtuales, sino que además nos permite gestionar nuestras versiones de Python de forma súper sencilla y limpia.

Llegando a poder desarrollar en Python sin ningún tipo de limitación, ¡sin tenerlo instalado en nuestra máquina!

Por último, destacar que en mi experiencia personal migrando unos cuantos proyectos de Poetry a uv, puedo decir que la mejora en el rendimiento es abismal y al principio impresiona. Con estas migraciones hemos conseguido reducir notablemente el tiempo de nuestras pipelines, lo que siempre es algo positivo para los equipos.

Os dejo por aquí varios repositorios en los que he aplicado todo lo comentado en el artículo por si os sirven de ayuda:

Un saludo, ¡espero que os haya gustado!