The 'No Upper Bounds' Dilemma: uv, Python, and the UX of Package Resolution
Astral’s uv has rapidly become a favorite tool in the Python ecosystem due to its extreme speed and consolidated features. However, its package management user experience (UX) has sparked intense debate, particularly regarding how it handles dependency resolution and updates.
A central critique of uv is that when a user runs uv add <package>, it writes the dependency to pyproject.toml without specifying an upper version bound (e.g., "pydantic>=2.13.4" instead of "pydantic>=2.13.4,<3.0.0"). Consequently, running uv lock --upgrade behaves like a "nuclear option," pulling in potentially breaking major version changes across the entire dependency tree.
While this behavior can be overridden using the --bounds flag or configuring add-bounds = "major" in pyproject.toml, the default "unsafe-by-default" behavior is a deliberate architectural choice. In Python, unlike in Node.js (npm/pnpm), it is impossible to load multiple diverging versions of the same package in a single runtime environment. If every library pinned upper bounds, dependency trees would frequently become impossible to resolve.
The Real Disagreement
The debate centers on whether a package manager should prioritize the safety of application developers (who want strict, SemVer-safe upper bounds to prevent breaking builds) or the flexibility of the broader library ecosystem (where upper bounds create resolution deadlocks).
As one prominent developer explained:
"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." — https://news.ycombinator.com/item?id=48230048
Another engineer highlighted the fundamental runtime limitation of Python that drives this choice:
"The problem is when you want to have two different incompatible versions of the same package
fooin the same program, because then you have to figure out whatimport foomeans." — https://news.ycombinator.com/item?id=48230426
Critics, however, argue that this places an unfair maintenance burden on application developers:
"In the eyes of uv, pydantic version 2, 3, and 100 are all perfectly acceptable. ... 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." — https://www.loopwerk.io/articles/2026/uv-ux-mess/
Why It Matters
This conflict exposes a deep runtime constraint in Python. While modern tools like uv can optimize installation speeds, they cannot bypass Python's single-namespace import system. As a result, developers building production applications must actively configure safety boundaries themselves, rather than relying on the package manager to provide a secure-by-default experience.