← Back to context

Comment by the_mitsuhiko

20 hours ago

> Note the lack of an upper bound

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.

Personally, I'd rather get an error from uv that packages aren't compatible when I run update, with a way to override that if needed, than get an error at runtime that may be difficult to track down to incompatible versions.

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.

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.

  • I'm not in front of my terminal, but I'm almost certain there is a way to do this. And if not, it would not be hard to add.

    I can't really take the article fully seriously when they are like "uv cant do this. Well actually it can but you gotta use an extra flag." It reads rather PEBKAC.

    • I think `uv lock -P <package-name>` to only update a particular package (and transitive deps of course).

As much as uv has improved the situation, I have to imagine that there's plenty of stuff like this that fundamentally is impossible to address via tooling. It's incredible how much the situation seems to have improved compared to before it was around, but it seems like things might never be totally good without the ecosystem as a whole making some breaking changes, and I'm guessing after the whole 2->3 situation there's not much appetite for something like that any time soon.

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?

  • Am I using it differently than everyone else? I don't want an upper bound, I want a specific version. So always ==, never >=, and upgrading a dependency is an explicit action. I don't want to suddenly have a never version.

    • That’s how the ‘sync’ flow works (recreating venv, through the lockfile), here discussing specifically updates.

That part of the article almost read like clickbait, because at the end he admits there is an upper bound arg:

> uv add pydantic --bounds major

So not really sure what he's complaining about

  • His lament isn't that uv lacks upper bounds.

    His point is that the cli experience of uv is badly designed. And having upper bounds an opt-in is another point he makes in that.

    Also, the overuse of the term "clickbait" for anything we don't agree with is ...clickbait.

    > So not really sure what he's complaining about

    It says it on the bloody title of his post: UV's "package management UX is a mess".

    He's complaining that upper bounds by default would the good choice, and that UV potentially nuking your deps by default is bad. And that the whole UX around uv updates is bad in general, for which he gives several examples.

    We can argue if he's right or wrong about each of those, but it's pretty clear what he complains about.

  • the article complains about ux, not capabilities. In this case it is complaining about the defaults.

Also it doesn't even matter because the real way to use both uv and npm is to switch everything to = and only update manually, rather than trusting non-major updates not to break anything

  • The distinction here is on application vs library, IMO. I basically agree that applications, as a default, `==`'ing everything makes sense.

    For libraries, having loose bounds might mean that users upgrade and hit issues due to a lack of an upper bound. But given how lightly maintained most projects are, the risk of upper bounds simply getting in the way are higher IMO.

    (Put an upper bound if you know of an issue, of course!)

    It's a bit tricky though. Django deps in particular tend to want to explicitly check support for newer versions, but the more I think about it the more I ask myself if this is the right strategy

  • Isn't there a lock file for that? I'm mostly a rust dev, but I thought I saw a lock file in a uv project I was vibe coding

    • The lockfile does more than just pin the versions of your immediate deps, so one might reset it for some other reason. Or you might want to update individual packages without caring about the specific commands for that, so you edit the package file, delete lockfile, reinstall.

      1 reply →

  • non major updates in the npm ecosystem are pretty reliable in my experience; my much more limited python experience suggests that semver is much less respected on that side of the fence

>If an upper bound were to be supplied you would end up with trees that can no longer resolve in practice.

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.

The entire purpose of semver is to give you a way to resolve that conundrum. New major version = assume it's incompatible.

I mean, it may not actually work, but that's what it's for.

  • The use or adherence to semver isn't the problem here. As you say, if a package follows semver, it's easy enough for the package managers to automatically update to newer compatible versions. The problem is when you want to have two different incompatible versions of the same package `foo` in the same program, because then you have to figure out what `import foo` means. You might say "just don't do that", but that package could be an indirect dependency of several of your direct dependencies. Some languages handle this natively, e.g. in Rust it just works if you have multiple versions of the same library in different parts of your dependency tree (and you'll get a compilation error if you try to pass a type from one version into a function of an incompatible version). But Python does not handle this use case very well.

  • > The entire purpose of semver is to give you a way to resolve that conundrum. New major version = assume it's incompatible.

    I'm not sure I'd agree with that characterization. The point of semver is that you can assume that certain types of bumps won't include certain types of changes, not that you assume that the types of changes that can happen in a type of bump will happen. A major version bump not breaking anything is completely valid semver, and breaking one function (which plenty of users might not use, or might use in a way that doesn't get broken) in an API with thousands is still technically only valid in a major version bump (outside of specific exceptions like the major version being 0).

    It's a subtle difference, and I'm optimistic that it's something you understand, but misunderstandings of semver seem so common that I can't help but feel like precision when discussing it is important. I've encountered so many smart people who misunderstand aspects of semver (and not just minutia like "what constraints are there on tags after the version numbers"), and almost all of them seemed to have stemmed from people learning a few of basic tenets of it and inferring how to fill in the large gaps in a way that isn't at all how its specified. The semver specification is pretty clear in my opinion even about where some of the edge cases someone less informed might assume, and if we don't agree on that as the definition, I don't know how we avoid the (completely realistic) scenario where everyone in the room has an idea of what "semver" means that's maybe 80% compatible with the spec, but the 80% is different for each of them, and trying to resolve disagreements when people don't agree about what words mean is really hard.

    • > not that you assume that the types of changes that can happen in a type of bump will happen

      … an assumption that something happened is not a definitive statement that it did happen, only that we're assuming it did, because it could happen, or perhaps here, that because the major was bumped, that it is legal, according to the contract given, for it to have possibly happened in a way that we depended on. They're not saying that it will/must; "assume a major version is incompatible" is not at odds with what you've written.

      2 replies →

  • There isn't a good way to know if a given package is using semver though.

    There's a lot of packages in the Python ecosystem that use time based versioning rather than semver (literally `year.minor`) and closed ranges cause untold problems.

  • Semantic versioning is about versioning individual dependencies, no? The issue here seems to be about transitive dependencies, where different versions of the same package is used by multiple packages which depend on it.

    uv's default being to always select the latest version seems to be what Clojure's tools.deps does.

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.