Uv is fantastic, but its package management UX is a mess

nchagnet1 pts0 comments

Loopwerk: uv is fantastic, but its package management UX is a mess For the first time since 2023 I'm available again for new projects! Hire me

uv is fantastic, but its package management UX is a mess<br>May 21, 2026 &bull; #python and #uv

Astral’s uv has taken the Python world by storm, and for good reason. It is blisteringly fast, handles Python versions with ease, and replaces a half-dozen tools with a single binary. I’ve written multiple articles about it before.<br>Getting started with a new Python project using uv and adding your first dependencies is very easy. But once you move past the initial setup and into the maintenance phase of a project, i.e. checking for outdated packages and performing routine upgrades, the CLI starts to feel surprisingly clunky compared to its peers like pnpm or Poetry.<br>Finding outdated packages<br>In my JavaScript projects, if I want to see what needs an update, I run:<br>$ pnpm outdated<br>This gives a clean, concise list of outdated packages, their current version, the latest version, and the version allowed by your constraints.<br>In uv, there is no uv outdated. Instead, you have to memorize the following mouthful:<br>$ uv tree --outdated --depth 1<br>The output is also a problem. It doesn’t just show you what is outdated; it shows you your entire top-level dependency tree, with a small annotation next to the ones that have updates available. If you have 50 dependencies and only two are outdated, you still have to scan a 50-line list.<br>Poetry isn’t much better with its command poetry show --outdated, but at least it only shows actual outdated packages.<br>Unsafe version constraints by default<br>This is the most significant philosophical departure uv takes from pnpm and Poetry, and it’s a dangerous one for production stability.<br>How pnpm/Poetry handle it<br>When you add a package using pnpm add, it writes it to package.json using the caret requirement (^1.23.4). The caret at the beginning means that any 1.x.x version is allowed, but it will not update to 2.0.0.<br>Poetry does the same by default, using a format like >=1.23.4,. I find this less readable than ^1.23.4, but the effect is the same.<br>In both cases, updates are safe by default . You can run pnpm update or poetry update every morning and have high confidence that your build won’t break due to a major API change (assuming the packages you depend on respect SemVer).<br>How uv handles it<br>When you run uv add pydantic, it inserts this into your pyproject.toml:<br>dependencies = [<br>"pydantic>=2.13.4",<br>Note the lack of an upper bound. In the eyes of uv, pydantic version 2, 3, and 100 are all perfectly acceptable.<br>This means uv updates are unsafe by default . If you run a bulk update, you aren’t just getting bug fixes; you are opting into every breaking change published by every maintainer in your dependency graph.<br>The bad UX of the upgrade command<br>The commands to actually perform an update in uv feel like they were designed for machines rather than humans.<br>If you want to update everything in pnpm or Poetry, it’s a simple pnpm update or poetry update command. In uv, you use:<br>$ uv lock --upgrade<br>THOUGHTS<br>Why isn’t this simply uv update or uv upgrade? Who designed this command line interface? It’s not uv lock --add or uv lock --remove either!<br>Because of the “no upper bounds” issue mentioned above, uv lock --upgrade is a nuclear option. It will upgrade every single package in your lockfile to their absolute latest versions, ignoring SemVer safety. And this includes deep, nested dependencies you’ve never heard of! Good luck, better hope there are no breaking changes anywhere.<br>Once you realize this is too risky, you’ll want to upgrade only specific packages. After scouring the subpar output of uv tree --outdated --depth 1 to find them, the syntax becomes a repetitive chore.<br>How pnpm does it:<br>$ pnpm update pydantic httpx uvicorn<br>How uv does it:<br>$ uv lock --upgrade-package pydantic --upgrade-package httpx --upgrade-package uvicorn<br>Having to repeat the --upgrade-package flag for every single item is a huge hassle when you want to update a bunch of packages. I don’t understand why the UX of uv’s commands is so poor.<br>There is hope: the bounds flag<br>Luckily uv has recently introduced a --bounds option for uv add:<br>$ uv add pydantic --bounds major<br>This produces the safer pydantic>=2.13.4, constraint we’ve come to expect. However, this is currently an opt-in feature. You have to remember to type it every time, and as of now, it is considered a preview feature.<br>Until --bounds major (or a similar configuration) becomes the default behavior, uv users are essentially forced to choose between two bad options:<br>Manually edit pyproject.toml to add upper bounds for every single dependency.<br>Live in fear that uv lock --upgrade will accidentally pull in a breaking major version change.<br>What I’d like to see<br>I love uv. Its speed is transformative, and the way it manages Python toolchains is second to none. But as a package manager, the developer experience for maintaining a project is...

update package outdated upgrade pnpm poetry

Related Articles