5 puntos por GN⁺ 2025-03-31 | 1 comentarios | Compartir por WhatsApp
  • Incluso los scripts pequeños para probar APIs requieren ajustar el entorno de ejecución por sus dependencias, pero usando el shebang de ejecución de uv se pueden convertir en una forma ejecutable de inmediato, sin pasos de instalación
  • El ejemplo jam_users.py entra a un REPL después de eliminar y crear usuarios con httpx, IPython y loguru para probar el endpoint /users de una API en Go
  • Con el enfoque tradicional hay que instalar paquetes globalmente en el Python del sistema o preparar manualmente un entorno virtual, lo que vuelve engorroso compartir y volver a ejecutar el script
  • Si se declaran dependencies = ["ipython", "httpx", "loguru"] en el encabezado # /// script y se ejecuta uv run jam_users.py, uv se encarga del entorno aislado y de instalar las dependencias
  • Al agregar el shebang #!/usr/bin/env -S uv run --script y permisos de ejecución, en sistemas Unix se puede ejecutar como ./jam_users.py con solo uv instalado

Declarar dependencias dentro del script

  • El ejemplo jam_users.py prepara datos de usuarios de prueba contra la API local http://localhost:4000/v1/users
    • Envía solicitudes a la API con httpx
    • Entra al REPL de IPython para revisar las respuestas y continuar las pruebas
    • Registra logs de eliminación y creación con loguru
  • El script básico sigue el flujo de inicializar la lista de usuarios y luego volver a cargar los datos de prueba
    • Obtiene la lista de usuarios existente con GET /v1/users
    • Llama a DELETE /v1/users/{id} para cada usuario
    • Envía la lista de usuarios preparada como JSON a POST /v1/users
    • Después abre el REPL con IPython.embed()
  • Para ejecutarlo con python jam_users.py, httpx, IPython y loguru ya deben estar instalados en el entorno de ejecución
  • También se puede instalar globalmente en el Python del sistema o crear un entorno virtual aparte, pero ambos métodos requieren preparación antes de ejecutar
    • La instalación global puede ensuciar el Python del sistema con paquetes
    • El enfoque con entorno virtual exige gestionar manualmente la creación, activación, instalación y ejecución
    • En ambos casos hace falta un Python del sistema compatible con los paquetes necesarios

Crear un script ejecutable directamente con uv

  • Si se coloca la etiqueta # /// script de uv al inicio del script, se pueden declarar dependencias dentro del archivo
# /// script

# dependencies = ["ipython", "httpx", "loguru"]

# ///
  • Con este encabezado, se puede ejecutar con uv run jam_users.py
    • uv crea un entorno virtual aislado para el script
    • Descarga e instala las dependencias necesarias
    • Ejecuta el script dentro del contexto de ese entorno virtual
  • Un shebang común de Python, como #!/usr/bin/env python, no puede aprovechar el encabezado de script de uv porque Python ignora el comentario # /// script
  • Si se coloca la llamada a uv directamente en el shebang, el script puede tratarse como un archivo ejecutable
#!/usr/bin/env -S uv run --script

# /// script

# dependencies = ["ipython", "httpx", "loguru"]

# ///
  • La bandera -S de env hace que la cadena siguiente se divida en argumentos separados y se pase a env
  • Después de otorgar permisos de ejecución con chmod +x jam_users.py, se puede ejecutar directamente así
./jam_users.py
  • Con este enfoque, si uv está instalado en el sistema Unix, se puede ejecutar el script sin instalar dependencias por separado ni administrar entornos virtuales
  • Al entregar scripts de Python complejos a otros usuarios, se reduce la carga de explicar largos pasos de preparación del sistema antes de ejecutarlos

1 comentarios

 
GN⁺ 2025-03-31
Comentarios de Hacker News
  • No me molesta UV en sí, sino en general la forma de controlar la ejecución del código mediante comentarios.
    Está bien usar comentarios para directivas del linter o notas de desarrolladores, pero si se trata de configuración o datos relacionados con la ejecución, me parece mucho mejor algo como UV_ENV = { "dependencies": { "requests": "2.32.3", "pandas": "2.2.3" } }.
    Ese enfoque es sintaxis válida de Python, usa estructuras de datos estándar en lugar de parsear comentarios arbitrarios, por lo que es fácil de generar y validar, y, sobre todo, respeta el principio de que el código debería ejecutarse igual aunque se eliminen todos los comentarios.

    • Estoy de acuerdo, pero quisiera ir un paso más allá.
      El enfoque propuesto también es una constante mágica que no hace nada en runtime, así que solo se parsea mediante análisis estático, y otra herramienta podría verla como código sin usar y eliminarla en vez de que uv la lea.
      Mejor sería que, después de import uv, se llamara directamente a uv para decirle qué hacer, por ejemplo uv.exec(dependencies=["clown"], python=">=3.10").
      La primera ejecución se haría con cualquier runtime de Python que pueda encontrar este paquete uv hipotético, y luego el paquete uv prepararía el entorno virtual y el runtime de Python, añadiría una bandera como una variable de entorno y haría re-exec(3).
      En el segundo runtime, uv.exec detectaría la bandera y no haría nada.
    • Esto no lo inventó uv; como otras herramientas, usa el estándar PEP 723.
      https://peps.python.org/pep-0723/
    • Es un punto válido, pero creo que parsear y evaluar código imperativo es mucho más difícil y menos flexible que dejarlo como datos declarativos siguiendo el principio de mínimo privilegio.
      Además, la línea shebang en sí también funciona en la práctica como un comentario.
      Lleva 45 años tan profundamente arraigada que la gente simplemente no suele ser consciente de que es un comentario del shell.
    • La condición de que “el código debería ejecutarse igual aunque se eliminen los comentarios” sigue siendo cierta.
      Si se instalaron las mismas dependencias, el código se ejecuta igual.
      Más que cambiar el significado del código en sí, cambia el entorno en el que se ejecuta, y en ese sentido no es distinto del comentario #!/bin/bash al inicio de un script de shell.
    • Totalmente de acuerdo, y espero que algo así termine estandarizándose.
      Dicho eso, es muy probable que uv no quiera ejecutar código para averiguar las dependencias, así que tendría que ser un subconjunto muy limitado de la sintaxis de Python.
      El solo hecho de que algo así sea necesario ya expone una debilidad del lenguaje.
      La sentencia import en sí debería poder transmitir toda la información sobre dependencias.
  • En los últimos meses este tema apareció con mucha frecuencia en HN; algunos ejemplos recientes son:
    https://news.ycombinator.com/item?id=43500124
    https://news.ycombinator.com/item?id=42463975
    Me gusta uv, pero me cuesta aceptar la expresión self-contained por dos motivos.
    Primero, para ejecutar el script uv ya tiene que estar instalado.
    Se podría hacer que un script de shell verifique si uv está instalado y, si no, lo instale con curlpipe, pero eso aumenta bastante el boilerplate y el enfoque de curlpipe tampoco es bueno.
    Segundo, crear automáticamente un entorno virtual en algún lugar del directorio home no es realmente self-contained.
    Aunque lo ejecutes una sola vez y luego borres el script, ese entorno virtual queda ahí ocupando espacio, y no encontré en la documentación de uv ninguna garantía de que limpie automáticamente esos entornos virtuales temporales.

    • La queja de que uv es necesario y, si no está, hay que instalarlo manualmente o usar curl | sh, es válida.
      Sin embargo, a medida que los gestores de paquetes empiecen a incluir uv en sus repositorios, será un problema cada vez menor.
      Por ejemplo, uv ya está disponible en Alpine Linux y Homebrew: https://repology.org/project/uv/versions
      Además, los metadatos de script en línea son un estándar de Python.
      Si el sistema no tiene uv y tampoco está empaquetado, pero sí tiene una versión de Python compatible con el script, se puede ejecutar con pipx: https://pipx.pypa.io/stable/examples/#pipx-run-examples
      pipx está empaquetado de forma mucho más amplia: https://repology.org/project/pipx/versions
    • Para resolver ese tipo de problema, intenté forzar una combinación de docker+uv shebang, y funciona hasta cierto punto.
      En una máquina cualquiera de desarrollo, Docker puede ser más común que uv, y como el artículo original habla de un proyecto de empresa, sonaba razonable.
      El problema es que por ahora no cachea nada, así que descarga todo en cada ejecución, lo cual se siente raro; probablemente se pueda arreglar con volúmenes.
      Sería algo así: https://hugojosefson.github.io/docker-shebang/#python
    • Normalmente, para ejecutar un programa en una computadora primero hay que instalar algo, así que instalar uv en sí no me parece tan terrible.
      Aun así, si al ejecutarlo descarga cosas desconocidas de internet, es difícil llamarlo self-contained; algo realmente self-contained se parece más a AppImage.
    • 100% de acuerdo.
      Con algo como py2exe se puede crear un “script de Python” self-contained.
      A los desarrolladores les causa muchos problemas, pero para los usuarios los problemas se minimizan.
    • Como pequeño complemento: gracias a la deduplicación de paquetes de uv, el virtualenv no ocupa espacio salvo que tenga dependencias únicas.
  • Con Nix también se usa el mismo enfoque, y la línea shebang se ve así:
    #! nix-shell -i python3 -p "python312.withPackages (pkgs: [ pkgs.boto3 pkgs.click ])"
    Con esto, lo único que se necesita en el sistema es Nix; ni siquiera hace falta tener Python instalado.

    • Es cierto, pero todavía hay muchos paquetes de PyPI que no están empaquetados en nixpkgs, así que no es un enfoque tan general como uv.
    • Tal como dice el texto original, uv instala Python al vuelo cuando lo necesita.
    • La misma técnica se puede aplicar a cualquier lenguaje.
      El ejemplo más obvio es bash con todas las dependencias especificadas, y también he llegado a crear scripts Rust rápidos de un solo archivo con shebang de Nix.
      https://nixos.wiki/wiki/Nix-shell_shebang
    • El requisito de tener Nix instalado es mucho más fuerte que el de tener uv.
    • Me da curiosidad cómo hacer lo mismo con nix shell, el comando basado en flakes, en lugar de nix-shell.
  • Como dicen otros comentarios, la afirmación de que es “self-contained” depende de que uv esté instalado.
    Si de verdad quieres un script de Python self-contained, vale la pena echarle un vistazo al compilador Nuitka.
    Lo usamos sin problemas en producción para servicios gRPC, y basta con ejecutar nuitka --onefile run.py.
    Como es un compilador, el binario resultante también puede ser más rápido que el programa Python original empaquetado con PyInstaller.
    En la página de GitHub del autor aparece la frase: “La misión de mi vida es seguir intentando hasta morirme de viejo crear el mejor Python Compiler posible”.
    https://nuitka.net/
    https://github.com/kayhayen

  • Me gusta mucho este patrón, pero lamentablemente no pude hacerlo funcionar bien con LSP.
    Uso pyright en Helix, y ni siquiera funciona ejecutando el editor con uv run hx script.py.
    Se puede hacer algo como uv run --with whatever-it-is-i-need hx script.py, pero empieza a haber cada vez más duplicación.

    • Estoy usando un script feo que hice, uve:
      $ cat ~/.local/bin/uve
      #!/bin/bash
      temp=$(mktemp)
      uv export --script $1 --no-hashes > $temp
      uv run --with-requirements $temp vim $1
      unlink $temp
      Ojalá pronto el editor soporte uv python find --script.
  • Se ve bastante útil.
    Me pregunto si uv es una opción más segura para distribuir proyectos basados en Python a largo plazo.
    Me recuerda al rug pull de hace unos 5 años, cuando usamos Anaconda para administrar dependencias y luego cambiaron las reglas: los clientes corporativos con organizaciones de más de 200 empleados ya no podían usar Anaconda gratis y tenían que pagar una licencia comercial.

    • uv tiene licencia MIT o Apache-2.0.
      Podrían dejar de desarrollarlo o mover el trabajo futuro a un fork con otra licencia, pero no pueden cambiar retroactivamente la licencia anterior, así que lo que existe hoy queda garantizado como open source.
      Si realmente te preocupa, puedes hacer un fork y mantenerlo sincronizado.
      En la práctica, esto aplica casi igual a cualquier otro proyecto OSS, así que no me preocuparía demasiado.
      Hasta donde sé, conda nunca fue open source y distribuía binarios.
      https://github.com/astral-sh/uv?tab=readme-ov-file#license
    • Entiendo que el rug pull de Anaconda fue por el lado del repositorio.
      Los paquetes de conda-forge todavía se pueden usar gratis.
      uv simplemente usa PyPI, así que si surge un problema puedes cambiar de uv a pip, Poetry u otra herramienta, y seguir trayendo los paquetes del mismo lugar.
    • Tengo entendido que, si un proyecto tiene un acuerdo de licencia de colaborador (CLA) y hace que los colaboradores cedan sus derechos de autor al dueño del proyecto, es posible relicenciarlo.
      Ese dueño terminará siendo adquirido por la peor persona rica que puedas imaginar.
      Revisé por encima la guía de contribución y los issues de uv, pero no vi ningún CLA; en PyTorch, en cambio, el CLA está al inicio de la guía de contribución.
      Aun así, debería haber existido un fork comunitario de la última versión FOSS de Anaconda.
      Con Redis sí ocurrió, y Redis también usa CLA: https://github.com/redis/redis/blob/unstable/CONTRIBUTING.md#redis-software-grant-and-contributor-license-agreement
      Nunca hay que firmar un CLA.
      Mejor contribuyamos solo a proyectos copyleft.
      Nos pagan demasiado como para trabajar gratis.
  • Para utilidades pequeñas, parece una buena alternativa de empaquetado frente a la contenedorización.
    Ahora tengo que convencer a todos mis colegas de que instalen uv.

    • uv es sorprendentemente rápido, así que eso ayudará.
    • Para nosotros, el obstáculo es que nuestro escáner de vulnerabilidades SCA todavía no funciona con uv.
  • Se parece a bundler/inline del lado de Ruby.
    Me alegra que Python tenga algo parecido, y en la práctica es muy cómodo.
    https://bundler.io/guides/bundler_in_a_single_file_ruby_script.html

  • Me pregunto si alguien logró hacer que esto funcione en Windows.
    Quería usar este truco para una herramienta de un mod de juego en la que estoy trabajando, pero no pude hacer funcionar el truco del shebang.

  • Este caso de uso hizo que me gustara uv, pero siento que el hecho de que un PEP oficial y muy útil no esté soportado por las herramientas oficiales de Python va en contra del Zen of Python.
    Desde mi punto de vista, fue la primera vez que Python dejó de venir con “batteries included”.
    Ahora también tengo dos gestores de dependencias de Python en el sistema.
    Sé que hay mucho que decir sobre la gestión de dependencias en Python, pero en los últimos años, si un proyecto tenía solo requirements.txt, me las arreglaba con el pip+venv básico.

    • En la especificación de compilación de Python hubo algo de esa tendencia.
      Recuerdo que pyproject.toml salió antes que la biblioteca tomllib.
      Así que durante algunas versiones había que especificar módulos en un lenguaje que Python no podía leer de forma predeterminada.
      Es una situación peor que tener una forma predeterminada de incluir metadatos que no se usan.
      Para empezar, por eso son metadatos; si no, simplemente habría sido sintaxis de Python.