- uv se distingue par sa rapidité, la gestion des versions de Python et l’intégration dans un binaire unique, mais l’UX de gestion des paquets reste encore maladroite en phase de maintenance
- Les paquets obsolètes peuvent être vérifiés avec
uv pip list --outdated, mais comme la commande n’est pas de premier niveau et se trouve sous un espace de noms compatible pip, sa découvrabilité est faible
uv add pydantic ajoute par défaut une contrainte sans borne supérieure comme pydantic>=2.13.4, ce qui autorise même les montées de version majeure
- La mise à niveau complète se fait avec
uv lock --upgrade, et pour plusieurs paquets précis il faut répéter --upgrade-package, ce qui rend la commande verbeuse
- Le réglage
add-bounds = "major" permet de définir des contraintes par défaut plus sûres, mais il s’agit d’une fonctionnalité en préversion et les applications ont besoin d’une UX de mise à jour plus intuitive
Les points forts de uv et les limites en phase de maintenance
- uv d’Astral se distingue par sa rapidité, la gestion des versions de Python et sa capacité à remplacer plusieurs outils par un binaire unique
- Démarrer un nouveau projet Python et ajouter une première dépendance est simple, mais une fois le projet entré en phase de maintenance, l’UX pour repérer les paquets obsolètes et effectuer des mises à jour régulières paraît plus maladroite qu’avec pnpm ou Poetry
- Les principaux irritants concernent la découvrabilité de la commande de vérification des paquets obsolètes, l’absence de borne supérieure dans les contraintes de version par défaut et la verbosité des commandes de mise à niveau
Vérifier les paquets obsolètes
- Dans les projets JavaScript,
pnpm outdated permet de voir de façon concise les paquets obsolètes, la version actuelle, la dernière version et la version autorisée par la contrainte
- uv ne propose pas de commande de premier niveau
uv outdated, et au départ la commande suivante servait d’alternative
$ uv tree --outdated --depth 1
uv tree --outdated --depth 1 ne filtre pas uniquement les éléments obsolètes : il affiche tout l’arbre des dépendances de premier niveau, puis ajoute une petite annotation à côté des éléments pouvant être mis à jour
- Même avec 50 dépendances et seulement 2 paquets obsolètes, il faut parcourir une liste de 50 lignes
poetry show --outdated a lui aussi un nom de commande moins intuitif, mais dans les faits sa sortie n’affiche que les paquets obsolètes
Le risque des contraintes de version par défaut
-
L’approche par défaut de pnpm et Poetry
pnpm add enregistre dans package.json une exigence caret comme ^1.23.4
^1.23.4 autorise les versions 1.x.x, mais pas la mise à jour vers 2.0.0
- Poetry utilise lui aussi par défaut un format comme
>=1.23.4,<2.0.0 ; c’est moins lisible, mais l’effet est le même
- Avec ces deux outils, tant que les paquets respectent SemVer, exécuter
pnpm update ou poetry update réduit le risque de casser le build à cause de changements majeurs d’API
-
L’approche par défaut de uv
uv add pydantic ajoute dans pyproject.toml une contrainte sans borne supérieure, comme ci-dessous
dependencies = [
"pydantic>=2.13.4",
]
- Avec cette contrainte, les versions 2, 3 ou 100 de pydantic sont toutes autorisées
- Lors d’une mise à jour massive, on ne récupère donc pas seulement des correctifs, mais aussi tous les changements incompatibles publiés par les mainteneurs à travers le graphe de dépendances
- En maintenance d’application, cela peut notamment créer un risque de stabilité
L’UX des commandes de mise à niveau
- Avec pnpm et Poetry, une mise à jour complète reste simple
$ pnpm update
$ poetry update
- Avec uv, la mise à niveau complète utilise la commande suivante
$ uv lock --upgrade
uv lock --upgrade n’est ni uv update ni uv upgrade, mais une option de la commande lock, ce qui le rend moins intuitif comme commande de gestion des paquets pour un humain
- Combiné à des contraintes sans borne supérieure,
uv lock --upgrade revient à pousser tous les paquets du lockfile vers la version la plus récente possible
- Cela inclut aussi des dépendances profondément imbriquées que l’on ne connaît pas forcément directement
- Pour mettre à jour seulement certains paquets, pnpm permet simplement d’énumérer leurs noms
$ pnpm update pydantic httpx uvicorn
- Avec uv, il faut répéter le flag
--upgrade-package pour chaque paquet
$ uv lock --upgrade-package pydantic --upgrade-package httpx --upgrade-package uvicorn
- Lorsqu’on veut monter plusieurs paquets d’un coup, cette répétition du flag devient une vraie gêne
Le flag --bounds et la configuration
- uv a récemment ajouté l’option
--bounds pour uv add
$ uv add pydantic --bounds major
- Cette commande génère une contrainte plus sûre :
pydantic>=2.13.4,<3.0.0
--bounds major est pour l’instant une fonctionnalité en préversion, à activer explicitement dans chaque commande
- Il s’est ensuite avéré qu’il est possible de définir une valeur par défaut une fois pour toutes dans
pyproject.toml
[tool.uv]
add-bounds = "major"
- Avec ce réglage, il n’est plus nécessaire de taper
--bounds major à chaque fois pour obtenir des valeurs par défaut plus raisonnables avec uv add
- Pour les applications, il vaudrait mieux que ce comportement soit celui par défaut, mais dans la pratique l’usage n’est pas aussi pénible qu’il avait été décrit au départ
La différence entre applications et bibliothèques
- Le conseil standard du packaging Python est qu’une bibliothèque publiée sur PyPI ne doit pas figer de borne supérieure, et ce conseil est pertinent
- Si toutes les bibliothèques fixent une borne supérieure, l’arbre de dépendances des consommateurs en aval peut devenir impossible à résoudre
- À l’inverse, une application est un nœud terminal du graphe de dépendances, et aucun autre utilisateur ne résout ses dépendances à partir de ces contraintes
- Pour une application, il n’y a pas de coût à poser une borne supérieure, et cela protège des montées inattendues de versions majeures
- Le sujet ici concerne la maintenance d’applications comme des sites web, des services ou des outils internes ; pour la distribution de bibliothèques, une valeur par défaut sans borne supérieure peut rester raisonnable
Les points corrigés et les problèmes qui restent
uv pip list --outdated permet d’afficher uniquement les paquets obsolètes
$ uv pip list --outdated
- Cela affaiblit la critique visant la sortie bruyante de
uv tree --outdated --depth 1
- Le problème qui reste est que cette fonctionnalité n’est pas une commande de premier niveau mais se trouve sous l’espace de noms compatible
pip, ce qui nuit à sa découvrabilité
- Le réglage
add-bounds = "major" permet de définir des bornes par défaut, donc l’alternative binaire entre tout éditer à la main ou accepter le risque n’est pas exacte
- Malgré cela, la fonctionnalité reste en préversion et la gestion des paquets pour les applications a toujours besoin de contraintes par défaut plus sûres et de commandes de mise à jour plus intuitives
Les améliorations souhaitées
- Il faut une commande dédiée
uv outdated qui montre clairement uniquement les paquets obsolètes
- Il faut une commande
update plus ergonomique, qui permette de mettre à jour plusieurs paquets sans répéter des flags
- Les contraintes de version par défaut devraient mieux refléter les attentes de stabilité liées au versionnage sémantique (SemVer)
- En l’état, il reste une charge mentale : il faut examiner avec suspicion chaque ligne modifiée dans le lockfile
1 commentaires
Réactions sur Hacker News
La plage de versions par défaut de
uv addpeut être définie comme un paramètre persistant, donc pas besoin de la repasser à chaque foisRéférence : https://docs.astral.sh/uv/reference/settings/#add-bounds
La raison pour laquelle il n’y a pas de borne supérieure par défaut est que cela crée beaucoup de conflits inutiles dans l’écosystème, et à l’époque où j’utilisais Poetry, j’avais aussi rassemblé des ressources à ce sujet : https://github.com/zanieb/poetry-relax#references
Dans un projet web, quand j’utilise des dépendances, je veux des bornes supérieures pour éviter les changements cassants, en partant du principe que les dépendances respectent SemVer
L’idée était d’encourager à ne pas mettre de bornes supérieures défensives, puis de construire en continu, à chaque release, une grande partie active de l’écosystème pour détecter les vrais problèmes de compatibilité, envoyer des alertes automatiques aux mainteneurs et donner un calendrier clair pour rester dans la prochaine release « LTS »
Aujourd’hui, le solveur de Cabal semble assez stable à lui seul, mais des builds nocturnes à grande échelle, avec des échecs et blocages visibles, ont probablement contribué à garder l’écosystème résoluble
add-bounds, et même si le verrouillage précis des dépendances est important, c’est utile pour les projets que des développeurs moins expérimentés pourraient facilement négliger, surtout quand il s’agit de produits finaux plutôt que de bibliothèques--native-tlsde façon permanenteÀ cause de la configuration Zscaler de mon entreprise, UV échoue toujours sans ce flag
Je me demande aussi s’il est prévu de prendre en charge la possibilité de spécifier une version de Python compatible avec une architecture donnée. Un package maintenu par mon entreprise doit utiliser Python 32 bits, donc je dois toujours passer
--python /path/to/32bitexclude-newerdanspyproject.tomlQuand j’exécute
uv run,exclude-newerest supprimé depyproject.tomlJe pourrais lancer
uv run —-frozenouuv run --exclude-newerà chaque fois, mais ça ne semble pas être le bon flux, donc je me demande s’il existe une manière idiomatique de faire que je rateuva besoin d’un résultat de résolution unique, donc l’absence de borne supérieure est un choix de conception intentionnelnpm peut installer des résolutions différentes à différents endroits de l’arbre, mais en Python ce n’est pas une option. J’ai dû prendre la même décision dans Rye, et il n’y a pas de meilleure solution
Mettre des bornes supérieures peut en pratique créer un arbre qu’on ne peut plus résoudre. Une partie de l’écosystème Python a même distribué des overrides pour de vieux packages publiés avec de mauvaises hypothèses sur les bornes supérieures
Aujourd’hui, on ne peut pas savoir si son package sera compatible ou non avec un package qui n’a pas encore été publié
uvune erreur disant que les packages ne sont pas compatibles au moment de la mise à jour, et pouvoir override si nécessaireC’est préférable à tomber sur des erreurs d’incompatibilité de version à l’exécution, difficiles à diagnostiquer
pyproject.tomlLe vrai problème, c’est que
uv lock —-upgrademet tout à niveau d’un coup dès qu’il n’y a pas de borne supérieureS’il existait un moyen de mettre à jour des packages sans faire monter leur version majeure, cette commande serait bien plus sûre
C’est infiniment mieux qu’avant uv, mais il semble difficile d’aller jusqu’au bout sans changements cassants à l’échelle de tout l’écosystème. Quand on repense à la transition de Python 2 vers 3, il ne semble pas y avoir beaucoup d’appétit pour ce genre de changement avant un moment
Le flag
—-boundaide, mais c’est encore une chose de plus à saisir et à retenirSi uv pouvait savoir que le projet concerné n’est pas une bibliothèque, peut-être pourrait-il mettre des bornes supérieures par défaut
=et à ne pas croire que les mises à jour non majeures ne casseront rien, puis à mettre à jour uniquement à la mainJ’ai 257 dépendances Python dans une application en production, dont plus de la moitié sont des dépendances directes
Je ne mets pas de bornes supérieures dans
pyproject.toml, et toutes les deux semaines j’exécuteuv lock --upgradevia GitHub ActionsJ’ai une bonne couverture de tests, donc si quelque chose casse, les tests échouent, et il y a aussi un flux de revue assisté par IA. Quand une PR de mise à jour est créée, un workflow IA liste les mises à jour de versions majeures et mineures via un script Python, va chercher les changelogs, les lie et les résume, puis analyse le niveau de risque de chaque package en fonction de la manière dont il est utilisé dans la base de code
En général, c’est presque indolore, et il n’est pas nécessaire de mettre à jour les packages un par un, de vérifier les vieux packages ou de gérer des dépendances négligées. Il est très rare qu’un changement nécessite une correction par l’auteur de la dépendance et ne puisse pas être résolu dans le code, environ une fois par an. Sur les trois derniers mois, il y a eu 18 hausses de version majeure, et une seule a nécessité une modification du code
J’aimerais faire pareil côté front-end, mais je n’ai pas assez de tests pour le faire en toute sécurité. Les tests backend sont plus faciles à écrire et plus importants, donc je pense que toutes les bases de code devraient en avoir. Avec des tests, on peut simplement tout mettre à jour automatiquement
Au moins pour transformer des consignes en langage naturel en tests précis
Cela fait un moment que je n’écris plus les tests moi-même à la main, alors qu’avant c’était quelque chose dont je me plaignais toujours, et ce n’est plus le cas
UV a beaucoup apporté à Python, mais aujourd’hui j’ai quand même bien galéré
J’essayais de centraliser la gestion de scripts dispersés dans plusieurs dépôts et implémentés différemment au fil du temps
L’idée était de faire
uv run --with $package main --help, avec le comportement automatique suivant : 1) installer puis exécuter si absent, 2) ne pas installer si déjà à jour, 3) mettre à jour si ce n’est pas la dernière versionMais les trois points se sont révélés plus compliqués que prévu. En pratique,
uv runréinstallait à chaque fois, avec 6 secondes pour l’environnement virtuel et l’installationuvxouuv tooln’étaient pas vraiment mieux, car ils introduisaient un nouveau problème : les utilisateurs ne recevaient pas les mises à jourJ’ai fini par faire un script qui interroge CodeArtifact avec un GET paginé, met à jour si une version non-développement plus récente existe, puis relance. Ça fonctionne, et 200 ms de latence valent mieux que 6 secondes, mais ce n’était pas l’expérience que je voulais
uv run --with $package main --helpdevrait avoir exactement ce comportement avec très peu de surcoûtIl ne devrait pas réinstaller à chaque fois ; l’environnement
--withest conservé dans le cache. Et même si l’environnement n’était pas mis en cache, les dépendances le sont, et les installer depuis le cache est très rapide. On devrait clairement être en dessous de 200 msSi vous ouvrez un exemple de reproduction détaillé, je peux regarder
uv tool installetuv tool upgradesemblent appropriésCela dit, ce genre de petite friction devrait être relativement facile à corriger, donc ce serait bien d’ouvrir un ticket
uv run mainCela installera, mettra en cache et exécutera automatiquement les dépendances nécessaires : https://docs.astral.sh/uv/guides/scripts/
uv tool upgradeJe ne sais pas si c’est exactement le même cas d’usage, mais c’était excellent pour synchroniser un écosystème de microservices en polyrepo
J’ai été assez surpris de voir
"uv tree --outdated --depth 1"recommandé pour afficher la liste des dépendances obsolètesPersonnellement, depuis que cette commande existe, j’utilise
"uv pip list --outdated"Cela dit, je suis d’accord pour dire qu’une commande aussi importante mériterait une sous-commande de premier niveau distincte
Il est vrai que
"uv pip list --outdated"fournit une sortie bien meilleure, merciMais le simple fait qu’il y ait deux façons de voir les packages obsolètes, avec des sorties très différentes, renforce aussi l’impression d’une UX bancale
"uv tree -od1"devrait probablement fonctionner aussiMais la critique adressée à des gestionnaires de paquets comme pacman, comme à apt, était aussi qu’il faut proposer des commandes faciles à comprendre pour les usages fréquents
Vu le titre qui parle de quelque chose d’« affreux », les exemples montrent surtout qu’il faut ajouter quelques arguments
Un meilleur titre serait plutôt améliorations de qualité de vie souhaitées dans UV
Le fond du retour est utile et je suis d’accord avec la plupart des points, mais ce genre de formulation diminue la valeur du feedback et appelle des réactions défensives
L’interface en ligne de commande de uv me semble aussi pénible par moments, mais je comprends pourquoi elle a été écrite ainsi
uvest excellent, mais aujourd’hui le plus gros problème du packaging Python reste de bien gérer le packaging scientifique / machine learningSi on veut installer PyTorch, il faut déjà déterminer quelle version, puis si c’est CUDA. Et si c’est CUDA, il y a encore six variantes selon la version de CUDA, sans compter que les wheels sont trop gros pour être mis sur PyPI et doivent être récupérés directement
Conda ne résout ce problème qu’en partie. Spack est extrêmement configurable et permet d’avoir les dépendances C/C++/Fortran et la toolchain de compilation nécessaires pour obtenir les meilleures performances, mais il s’intègre mal avec uv et les autres. Résultat : il est difficile d’emmener en production un projet expérimental de machine learning créé par des chercheurs
Au final, on retombe sur la situation évoquée plus haut
Il y a beaucoup de retours utiles, mais aussi une part de formulation clickbait
Concernant
pnpm outdated, ce n’est pas quelque chose qui revenait très souvent jusqu’ici, mais cela semble être une demande raisonnable. Cela vient probablement de différences culturelles entre Python et JavaScript. En Python, je me suis rarement soucié du fait qu’une dépendance soit obsolète tant qu’elle n’était pas vulnérable ou cassée, alors que dans l’écosystème JavaScript, il semble assez courant de mettre à niveau quand l’occasion se présente. Ce n’est pas pour dire que c’est mauvais, mais cela montre à quel point les intuitions sur ce qu’une interface en ligne de commande doit exposer peuvent différer entre grandes communautés de programmationComme l’a dit Armin, le comportement de uv sur les bornes supérieures est intentionnel, et fonctionnellement nécessaire compte tenu de la manière dont Python résout les dépendances. C’est un compromis propre à Python par rapport à d’autres langages, mais je pense que c’est un bon compromis, dans la mesure où il n’y a qu’une seule version de chaque dépendance dans l’arbre et que toutes les exigences entremêlées sont résolues en fonction de cela
Si
uv lock --upgradefonctionne ainsi, c’est parce qu’il met à jour le fichier de verrouillage, pas les exigences de l’utilisateur. À l’inverse,pnpm updatesemble mettre à jour les exigences utilisateur danspackage.json. Cela peut prêter à confusion, mais le fait de le placer sousuv lockest plus exact. Sinon, des utilisateurs risqueraient d’être déroutés parce queuv upgradene ferait pas la mise à niveau qu’ils imaginent. Il y a sans doute encore de la place pour une présentation plus propre, et il y a manifestement une demande pour une sous-commande uv qui mette aussi directement à jour les exigences elles-mêmeshttps://news.ycombinator.com/item?id=48230048
La commande
uv lockqui ne touche qu’au fichier de verrouillage a du sens, mais les utilisateurs ont aussi un vrai besoin de mettre à niveau les dépendances directes et transitives. Pour les transitives,uv lock --upgrade-packagele permet, mais c’est un peu verbeux. Cela fonctionne aussi pour les dépendances directes, mais sans modifierpyproject.toml, alors que ce fichier est bien plus visible pour les développeursSi les versions de packages dans
uv.lockprennent de l’avance surpyproject.toml, alorspyproject.tomldevient moins fiable comme guide pour comprendre la surface de dépendances. Une commandeuv upgradeconviviale serait bienvenueLe plus gros piège UX que j’ai vu jusqu’ici avec uv, c’est
uv pip. Beaucoup de projets utilisent correctement uv avecpyproject.tomletuv.lockpour le développement, mais dans les Dockerfiles de déploiement ou les outils CI, ils fontuv pip install -r pyproject.toml, ce qui contourneuv.lockLe fait que les agents de code recommandent de mauvais schémas
uv pipparce qu’il y a trop depipdans leurs données d’entraînement pose problème, mais uv devrait aussi offrir des garde-fous pour protéger les utilisateursuv est un excellent outil et devrait être plus largement adopté : https://aleyan.com/blog/2026-why-arent-we-uv-yet
poetry updatemet aussi à jour le fichier de verrouillage. Je trouve que la manière dont l’interface CLI de uv est structurée est assez pénible au quotidien. On a l’impression qu’elle est conçue pour l’exactitude et pour les machines, pas pour être convivialeuv pip list --outdatedquand j’ai besoin de cette informationuv upgradeest sur la feuille de routeSi cela n’a pas encore été fait, c’est parce qu’il est difficile d’en faire une excellente expérience, qu’il y a bien plus de nuances que ce que les gens imaginent, et que l’équipe est petite avec beaucoup de priorités
pnpm outdatedest de voir ce qui serait mis à jour si l’on exécutait"uv sync --update"ou"uv lock --update"Cela dit, il vaudrait peut-être mieux ajouter une demande de confirmation à ces commandes
Pixi utilise uv comme backend, et j’ai apprécié son interface parce qu’il est facile d’ajouter un alias de tâche qui liste joliment les packages obsolètes
En particulier, Pixi-diff-to-markdown rend les mises à jour automatiques de packages en CI faciles à parcourir
Si vous voulez voir lesquels des packages obsolètes seraient mis à jour, vous pouvez créer dans votre projet un alias de tâche comme celui-ci
pixi task add outdated "pixi update --dry-run --json | pixi exec pixi-diff-to-markdown"Puis exécuter
pixi run outdateddans le projetLa sortie est un tableau Markdown lisible avec les packages qui seraient mis à jour, leur version actuelle et la nouvelle version qui serait installée par la commande
pixi update. Bien sûr, cela dépend aussi des préférences et du contexteRécemment, un script
envest apparu dans mon PATH et a perturbé l’usage normal de la commande UNIXenvJ’ai découvert que c’était créé par l’installateur uv quand on exécute la commande ci-dessous
curl -LsSf [https://astral.sh/uv/install.sh](<https://astral.sh/uv/install.sh>) | shIl installe dans
$HOME/.cargo/bin/, puis crée un script shell dans$HOME/.cargo/envqui préfixePATHavec$HOME/.cargo/bin/si ce chemin n’y est pas déjàJ’ai du mal à comprendre quels programmeurs écrivent du code qui écrase ainsi des commandes UNIX de base