Since uv needs a singular resolution that's entirely intentional. In npm you can install diverging resolutions for different parts of the tree but that is not an option with Python. I had to make the same decision in Rye and there is just no better solution here.
If an upper bound were to be supplied you would end up with trees that can no longer resolve in practice. Some package ecosystems in Python even went as far as publishing overrides for old packages that got published with assumed upper bounds that ended up wrong.
Don't forget that you cannot know today if your package is going to be compatible or incompatible with a not yet released package.
The lack of an upper bound in pyproject.toml isn’t the real problem. The real problem is that `uv lock —-upgrade` does a wholesale upgrade of everything without an upper bound. If there was a way to upgrade packages without updating the major version, this command would be a lot safer to run.
What you’re saying makes sense for library authors. But when I make a website and I depend on a bunch of packages, that’s where I want to be safe when upgrading and I want that upper bound. The —-bound flag really helps, but is one more thing to type and remember.
Maybe when uv knows the project isn’t a library it could default to upper bounds?
isn't ~= supported by uv or the discussion is about uv not adding it when package is added by command line rather than editing pyproject file? ~= is standard practice for me, as I always rather edit the file than memorize the commands.
Arguably the other reason for a lack of a default upper bound stems from PyPI has never required semver for hosted packages, there are plenty of packages on PyPI still using calver or marver or other more bespoke version schemes. (Whereas npm has always made semver an assumption/"requirement" for hosted packages.)
As someone part of the problem with a couple ancient packages in PyPI that are calver (though arguably I'd be surprised if many depended on them), I wonder if it is too late for more of the Python ecosystem to shift more directly to semver by default/everywhere as some of the other ecosystems now are.
Interesting point of view and I think feedback is good. Although I agree with the overall sentiment of the article, I disagree with the intensity of the criticism.
Having a command runner within your project will mask a lot of the issues the author mentioned. And although, in my experience, having a command runner for mid-sized projects and up is useful for many things, masking the UX issues means there's a problem.
I got on the uv bandwagon relatively recently as most of my work is maintaining older python projects so I've been using it for new builds. Although the speed part is welcome, I couldn't see what the big deal is and mostly keep on using it because it is a popular tool(there are benefits to that in my line of work) and not necessarily because it can do something that couldn't be done before though with a couple of other tools. Whether it is beneficial or detrimental to having all of that functionality within one tool, to me, is a matter of opinion.
The problem to me is that I've seen this cycle many times before. New tool shows up claiming it is far superior to everything else with speed being a major factor and everyone else is doing it wrong. Even though the new tool does a fraction of what the old "bad" tool is doing. With adoption comes increased functionality and demands and the new tool starts morphing into the old tool with the same claimed downsides. The UX issues to me are a symptom of that process.
I still think uv is a fine tool. I've used poetry before and sometimes plain old pip. They're all fine with each tool catering to different use cases, in my opinion. Sometimes you have to add pyenv, sometimes you don't. Sometimes you add direnv, sometimes you don't and so on. And I've cursed at everyone of them at times. However, the fanboyism is very strong with uv which makes me wonder why.
For me, and I suspect many others, the big deal is that it makes env management simply disappear. Before uv I used conda+poetry for years, and there was always the need to activate the env before doing anything (I used autoenv and ended up with .env files containing "conda activate <env_name>" in every project), and various other small pains that I actually became accustomed to over the years, but then I'd always be surprised when someone said X didn't work when it was fine for me. The uv came and I felt a great relief that I didn't know I was missing, and suddenly there were also no more surprises.
Much of this is useful feedback, even if phrased in a clickbait style. Some thoughts:
- Re: `pnpm outdated`: this is something that hasn't come up very much, even though it seems reasonable to me. I suspect this comes down to cultural differences between Python and JavaScript -- I can't think of a time when I've cared about whether my Python dependencies were outdated, so long as they weren't vulnerable or broken. By contrast, it appears to be somewhat common in the JavaScript ecosystem to upgrade opportunistically. I don't think this is bad per se, but seems to me like a good demonstration of discontinuous intuitions around what's valuable to surface in a CLI between very large programming communities.
- As Armin notes[1], uv's upper bound behavior is intentional (and is a functional necessity of how Python resolution works at large). This is a tradeoff Python makes versus other languages, but I frankly think it's a good one: I like having one copy of each dependency in my tree, and knowing that _all_ of my interdependent requirements resolve to it.
- `uv lock --upgrade` is written like that because it upgrades the lockfile, not the user's own requirements. By contrast, `pnpm update` appears to update the user's own requirements (in package.json). I can see why this is confusing, but I think it's strictly more precise to place under `uv lock`; otherwise, we'd have users with competing intuitions confused about why `uv upgrade` doesn't do their idea of what an upgrade is. Still, it's certainly something we could surface more cleanly, and there's been clear user demand for a uv subcommand that also upgrades the requirements directly.
> `pnpm outdated`: this is something that hasn't come up very much, even though it seems reasonable to me.
One use for it is to see what would be updated by running "uv sync --update" or "uv lock --update". Although that might be better served by having a confirmation prompt for those commands.
Agree with you on outdated and upper bounds. However, if users are complaining about the interface being difficult, there is probably something there.
Yes, it makes sense that `uv lock` commands only work the lock file, but users have real needs to upgrade direct and transitive dependencies. For transitive dependencies `uv lock --upgrade-package` works, even if a bit wordy. For direct dependencies, `uv lock --upgrade-package` also works, but doesn't touch `pyproject.toml`, which is much more developer visible. As `uv.lock` package versions get ahead of `pyproject.toml`, `pyproject.toml` becomes a less dependable guide to the surface area of dependencies. A friendly `uv upgrade` command would be nice.
The biggest uv ux footgun I have seen by far is `uv pip`. I have seen a lot of projects use uv correctly with pyproject.toml/uv.lock for development, but then use `uv pip install -r pyproject.toml`, which bypasses uv.lock, in their deployment Dockerfiles and ci tooling. Yes, coding agents are to blame for recommending bad `uv pip` patterns because they have so much `pip` in their training sets, but uv should provide some affordances to protect the user.
Sorry for the rant, uv is a great tool, that I think[0] should be used more! Thank you for your contributions to the ecosystem.
fwiw `uv upgrade` is on the roadmap — we just haven't done it yet because it's hard to build a great experience for it (there are far more nuances than people expect) and we're a small team with a lot of priorities.
UV has done so much for Python but I did fight it a bit today.
I was trying to centralize the management of a script that appears in a few different repos, and has invariably drifted in its implementation in multiple way over time.
My idea was
uv run --with $package main --help
I was looking for an easy way to automatically
1. Install it if it doesn’t exist and run
2. Don’t install it if it’s running the latest version
3. Update if it’s not on the latest version
All three were surprisingly tricky to accomplish.
By default uv run will reinstall it every time. Which is 6 seconds of venv and installs
uvx or uv tool weren’t much better as that posed new problems where a user wouldn’t get upgrades.
I ended up having the script run a paginated GET on codeartifact and update if there’s a newer non-dev version (and then re-execute).
That seems to work. And 200ms delay is better than 6 seconds. But it wasn’t quite the experience I wanted.
I'm a bit confused, `uv run --with $package main --help` should do what you say with very little overhead. We won't reinstall it every time, `--with` environments are stored in the cache and retained. Even if the environment is cached, the dependency is cached and installing from the cache is very fast (<200ms for sure).
Please feel free to open a reproduction with details and we can look into it.
You can also define required dependencies in a doc block at the top of the file and then just “uv run main” and it will automatically install and cache any dependencies and run. https://docs.astral.sh/uv/guides/scripts/
And then you look it up once, and now you know what it means forever. By contrast, the former expression is much wider with more going on, and furthermore you can't skim past it being sure nothing funny is going on because it may or may not be a range compatible with the latter form.
On it being a "mess": When writing applications, I only devoted at most 2 brain cells to installing dependencies even before Claude existed, and that was enough for npm, uv, or cargo to just work. Never used any of these flags. Meanwhile pip etc demand way more attention to not ruin things.
The author seems to use uv with Python (somewhat) fundamentally differently that I do.
I don't expect uv to do anything fancy. I don't run module management (installation, upgrades) through uv commands. I don't much care what uv's syntax is. I let each tool do one thing.
- Tool 1: UV makes a venv for each project, with whatever Py version is suitable for that project, so that each project's dependencies do not collide with one another.
- Tool 2: Pip installs all the requirements for a given project, within the venv for that project. From a requirements.txt file. Which, as far as I am aware, pip commands and requirements fit more of what they author is looking for.
I don't think it's necessary to subject oneself to the things the author (articulately) complains about, e.g.: uv's command syntax for listing packages; uv's emitting unbounded requirements syntax; uv's command to upgrade modules
Then again, it's quite possible the author is managing modules in projects with more complex needs than I am.
Long-winded example:
# Make an env for the project with appropriate Python and use it
uv venv ~/.venvs/myprojpy312 --python 3.12
source ~/.venvs/myprojpy312/bin/activate
# Make sure pip exists and is up to date
python -m ensurepip --upgrade
python -m pip install --upgrade pip
# Fill in requirements.txt in readable/meaningful syntax per needs
$ cat requirements.txt
requests>=2.31.0,<3.0.0
black==24.4.2
# Install the requirements initially (or again after changing requirements.txt)
python -m pip install -r requirements.txt
# List outdated modules
python -m pip list --outdated
# Upgrade modules, respecting the constraints
python -m pip install --upgrade -r requirements.txt
And in the age of the supply chain attacks, requiring a certain staleness could be useful, too (providing time to catch recent and revoke reasonably major and recently discovered issues, though at the cost of also blocking recent fixes):
$ cat ~/.config/uv/uv.toml
exclude-newer = "7 days"
# per https://news.ycombinator.com/item?id=47884491
Am I doing it wrong? Should I be thinking about `uv lock --upgrade`, `uv add`, and `uv tree --outdated` like the author? I'd rather just avoid all that, and have been able to so far.
We prefer not to add upper bounds by default because it causes a lot of unnecessary conflicts in the ecosystem. I previously collected some resources on this back when I used Poetry :) see https://github.com/zanieb/poetry-relax#references
The Haskell community wrestled with this for many years - may still I haven't really kept up. Last I was involved the most successful thing going on was Stackage where they encouraged no defensive upper bounds and ran continous builds on a large chunk of the most active parts of the ecosystem with every release to find actual compatibility issues, with automation to handle notifications back to owners and clear timelines if you wanted to stay in the next "LTS" release.
Things now seem to be stable enough that you just need the Cabal solver - but I suspect the broad nightly builds and visible failures/blockages probably help keep the ecosystem solvable.
I like using uv environments better for some stuff instead of Conda environments because of the speed difference. For example, I just tried installing Nvidia's Sana via Conda environment and my system froze during the wheel building phase. So, won't be using Sana as I can't convert the Conda environment script into a uv environment script. Too many errors pop up, even with the help of a coding agent handling the conversion.
Pixi uses uv as a backend, and I've enjoyed the UI because it's easy to add task aliases for things like nicely-formatted lists of outdated packages. (I have no affiliation with the project.)
Pixi-diff-to-markdown in particular has made scanning automated CI package updates easier. So for something like viewing outdated packages that would be updated, I'd do something like create a task alias for a project:
The output is a readable Markdown table of packages that would be updated, old version, and the new version that would be installed using the pixi update command. Your mileage and tastes, of course, may vary.
UX issues are the least important; it's just a matter of minutes for AI to improve, but you need to report it on GitHub.
The "unsafe version" is an unreasonable accusation. Do people really believe in semantic versioning—that not bumping the major version means it's safe? It also creates hard-to-resolve compatibility problems, causing more issues than it solves. Not capping dependency versions has basically become the consensus.
I would argue the current defaults for uv are the correct ones. Unless you have actually verified that said library follows semvar, and you know the library will break your code in the next major release, you should never use upper bounds. You should be using CI to manage updates of lock files (e.g. dependabot, renovate), and not blindly updating lock files. Similarly, you should care about your dependency tree, and not just direct dependencies. I feel the author thinks Python behaves the same way as the npm ecosystem, and thinks the same lessons apply.
Hard agree with this article. I love uv for so many things, but updating dependencies to latest versions that are compatible with everything else in the project is so much more painful in uv than in poetry. The fact that half the functionality is hidden behind `uv pip ...` or is incredibly odd as these days most people would never otherwise need `uv pip` at all.
Possibly the most frustrating hidden command is `uv pip show {package}` - a) why is it hidden inside the pip subcommand, and b) why is it missing the package's homepage that you get from `pip show {package}`? Given that actually upgrading stuff with `uv` often means going off and finding version numbers yourself, removing the homepage URL from the show output just feels spiteful.
Upper bounds only make sense if you assume that every package uses SemVer and that the author’s idea of a breaking change is the same as yours, which is a giant assumption that a package manager should not be making.
57 comments
[ 2.4 ms ] story [ 57.3 ms ] threadthen cites two examples where you have to write a couple extra args..
better title: “QOL changes i wish UV had”
Since uv needs a singular resolution that's entirely intentional. In npm you can install diverging resolutions for different parts of the tree but that is not an option with Python. I had to make the same decision in Rye and there is just no better solution here.
If an upper bound were to be supplied you would end up with trees that can no longer resolve in practice. Some package ecosystems in Python even went as far as publishing overrides for old packages that got published with assumed upper bounds that ended up wrong.
Don't forget that you cannot know today if your package is going to be compatible or incompatible with a not yet released package.
Maybe when uv knows the project isn’t a library it could default to upper bounds?
> uv add pydantic --bounds major
So not really sure what he's complaining about
And then we'd have to run uv again with an argument to not have upper bounds. Oh, the humanity!
As opposed, to it nuking our dependencies with incompatible packages.
Doesn't sound like the unsafe option should be the default.
As someone part of the problem with a couple ancient packages in PyPI that are calver (though arguably I'd be surprised if many depended on them), I wonder if it is too late for more of the Python ecosystem to shift more directly to semver by default/everywhere as some of the other ecosystems now are.
Having a command runner within your project will mask a lot of the issues the author mentioned. And although, in my experience, having a command runner for mid-sized projects and up is useful for many things, masking the UX issues means there's a problem.
I got on the uv bandwagon relatively recently as most of my work is maintaining older python projects so I've been using it for new builds. Although the speed part is welcome, I couldn't see what the big deal is and mostly keep on using it because it is a popular tool(there are benefits to that in my line of work) and not necessarily because it can do something that couldn't be done before though with a couple of other tools. Whether it is beneficial or detrimental to having all of that functionality within one tool, to me, is a matter of opinion.
The problem to me is that I've seen this cycle many times before. New tool shows up claiming it is far superior to everything else with speed being a major factor and everyone else is doing it wrong. Even though the new tool does a fraction of what the old "bad" tool is doing. With adoption comes increased functionality and demands and the new tool starts morphing into the old tool with the same claimed downsides. The UX issues to me are a symptom of that process.
I still think uv is a fine tool. I've used poetry before and sometimes plain old pip. They're all fine with each tool catering to different use cases, in my opinion. Sometimes you have to add pyenv, sometimes you don't. Sometimes you add direnv, sometimes you don't and so on. And I've cursed at everyone of them at times. However, the fanboyism is very strong with uv which makes me wonder why.
Much of this is useful feedback, even if phrased in a clickbait style. Some thoughts:
- Re: `pnpm outdated`: this is something that hasn't come up very much, even though it seems reasonable to me. I suspect this comes down to cultural differences between Python and JavaScript -- I can't think of a time when I've cared about whether my Python dependencies were outdated, so long as they weren't vulnerable or broken. By contrast, it appears to be somewhat common in the JavaScript ecosystem to upgrade opportunistically. I don't think this is bad per se, but seems to me like a good demonstration of discontinuous intuitions around what's valuable to surface in a CLI between very large programming communities.
- As Armin notes[1], uv's upper bound behavior is intentional (and is a functional necessity of how Python resolution works at large). This is a tradeoff Python makes versus other languages, but I frankly think it's a good one: I like having one copy of each dependency in my tree, and knowing that _all_ of my interdependent requirements resolve to it.
- `uv lock --upgrade` is written like that because it upgrades the lockfile, not the user's own requirements. By contrast, `pnpm update` appears to update the user's own requirements (in package.json). I can see why this is confusing, but I think it's strictly more precise to place under `uv lock`; otherwise, we'd have users with competing intuitions confused about why `uv upgrade` doesn't do their idea of what an upgrade is. Still, it's certainly something we could surface more cleanly, and there's been clear user demand for a uv subcommand that also upgrades the requirements directly.
[1]: https://news.ycombinator.com/item?id=48230048
One use for it is to see what would be updated by running "uv sync --update" or "uv lock --update". Although that might be better served by having a confirmation prompt for those commands.
Yes, it makes sense that `uv lock` commands only work the lock file, but users have real needs to upgrade direct and transitive dependencies. For transitive dependencies `uv lock --upgrade-package` works, even if a bit wordy. For direct dependencies, `uv lock --upgrade-package` also works, but doesn't touch `pyproject.toml`, which is much more developer visible. As `uv.lock` package versions get ahead of `pyproject.toml`, `pyproject.toml` becomes a less dependable guide to the surface area of dependencies. A friendly `uv upgrade` command would be nice.
The biggest uv ux footgun I have seen by far is `uv pip`. I have seen a lot of projects use uv correctly with pyproject.toml/uv.lock for development, but then use `uv pip install -r pyproject.toml`, which bypasses uv.lock, in their deployment Dockerfiles and ci tooling. Yes, coding agents are to blame for recommending bad `uv pip` patterns because they have so much `pip` in their training sets, but uv should provide some affordances to protect the user.
Sorry for the rant, uv is a great tool, that I think[0] should be used more! Thank you for your contributions to the ecosystem.
[0] https://aleyan.com/blog/2026-why-arent-we-uv-yet
I was trying to centralize the management of a script that appears in a few different repos, and has invariably drifted in its implementation in multiple way over time.
My idea was
uv run --with $package main --help
I was looking for an easy way to automatically
1. Install it if it doesn’t exist and run 2. Don’t install it if it’s running the latest version 3. Update if it’s not on the latest version
All three were surprisingly tricky to accomplish.
By default uv run will reinstall it every time. Which is 6 seconds of venv and installs
uvx or uv tool weren’t much better as that posed new problems where a user wouldn’t get upgrades.
I ended up having the script run a paginated GET on codeartifact and update if there’s a newer non-dev version (and then re-execute).
That seems to work. And 200ms delay is better than 6 seconds. But it wasn’t quite the experience I wanted.
Dunno if it's your exact use case but it's been amazing for keeping a polyrepo microservice ecosystem in sync.
Please feel free to open a reproduction with details and we can look into it.
(I work on uv)
What???
I understood the first format instantly, but had no idea what the second meant until the author explained it.
I don't expect uv to do anything fancy. I don't run module management (installation, upgrades) through uv commands. I don't much care what uv's syntax is. I let each tool do one thing.
- Tool 1: UV makes a venv for each project, with whatever Py version is suitable for that project, so that each project's dependencies do not collide with one another.
- Tool 2: Pip installs all the requirements for a given project, within the venv for that project. From a requirements.txt file. Which, as far as I am aware, pip commands and requirements fit more of what they author is looking for.
I don't think it's necessary to subject oneself to the things the author (articulately) complains about, e.g.: uv's command syntax for listing packages; uv's emitting unbounded requirements syntax; uv's command to upgrade modules
Then again, it's quite possible the author is managing modules in projects with more complex needs than I am.
Long-winded example:
And in the age of the supply chain attacks, requiring a certain staleness could be useful, too (providing time to catch recent and revoke reasonably major and recently discovered issues, though at the cost of also blocking recent fixes): Am I doing it wrong? Should I be thinking about `uv lock --upgrade`, `uv add`, and `uv tree --outdated` like the author? I'd rather just avoid all that, and have been able to so far.As a note, you can set the default bounds for `uv add` in persistent configuration — no need to provide it every time. See https://docs.astral.sh/uv/reference/settings/#add-bounds
We prefer not to add upper bounds by default because it causes a lot of unnecessary conflicts in the ecosystem. I previously collected some resources on this back when I used Poetry :) see https://github.com/zanieb/poetry-relax#references
Things now seem to be stable enough that you just need the Cabal solver - but I suspect the broad nightly builds and visible failures/blockages probably help keep the ecosystem solvable.
My flow is exact pins always, Renovate/Dependabot to inform me of new versions, or uv tree --outdated.
I personally use "uv pip list --outdated" since it has been introduced.
I agree that this is such an important command that it deserves its own top-level subcommand, though.
Pixi-diff-to-markdown in particular has made scanning automated CI package updates easier. So for something like viewing outdated packages that would be updated, I'd do something like create a task alias for a project:
pixi task add outdated "pixi update --dry-run --json | pixi exec pixi-diff-to-markdown"
And then run the task in the project via:
pixi run outdated
The output is a readable Markdown table of packages that would be updated, old version, and the new version that would be installed using the pixi update command. Your mileage and tastes, of course, may vary.
The "unsafe version" is an unreasonable accusation. Do people really believe in semantic versioning—that not bumping the major version means it's safe? It also creates hard-to-resolve compatibility problems, causing more issues than it solves. Not capping dependency versions has basically become the consensus.
Isd requires uv.
uv? Not installed. Ok, pip install uv. No way, "externally managed" whatever that means.
Ok, install venv. Then install uv. Then install isd. All messages are cryptic, some exceptions were thrown on console for quite vanilla default cases.
Possibly the most frustrating hidden command is `uv pip show {package}` - a) why is it hidden inside the pip subcommand, and b) why is it missing the package's homepage that you get from `pip show {package}`? Given that actually upgrading stuff with `uv` often means going off and finding version numbers yourself, removing the homepage URL from the show output just feels spiteful.