Map

Default version bound constraints

Wiki conceptpackage-managementsemverdeveloper-experience โ†ณ show in map Markdown
title
Default version bound constraints
type
concept
summary
Whether a package manager's add command writes a SemVer-respecting upper bound by default, and the consequences when it doesn't
tags
package-management, semver, developer-experience
created
2026-05-22
updated
2026-05-22

When you run npm add X, cargo add X, poetry add X, or uv add X, the tool picks the version constraint that will end up in your manifest. That default is a small decision with long-running consequences, because most projects accept whatever the tool wrote and never revisit it.

The two camps:

Ecosystem Default for add X (current version 1.2.3) Upper bound?
npm / pnpm / yarn ^1.2.3 (caret = compatible major) Yes โ€” implicit at major-version line
Cargo (Rust) 1.2.3 (caret implied; same as ^1.2.3) Yes
Poetry (Python) >=1.2.3,<2.0.0 (or ^1.2.3 in newer versions) Yes
Bundler (Ruby) ~> 1.2 (pessimistic constraint) Yes
uv (Python) >=1.2.3 No
pip-tools (Python) exact pin from compile Yes (over-strict)

The case for an implicit upper bound: SemVer says a major-version bump is allowed to break public API, so a tool that doesn't pin the major-version line is opting every project into eventual breakage at update/upgrade time. The case for no upper bound (uv's choice): SemVer is aspirational not enforced, upper bounds cause resolver pain when a dependency releases a major bump that doesn't actually break you, and Python in particular has a long history of unhelpful upper-bound conflicts (poetry add famously refused to install many libraries because of transitive upper bounds the libraries didn't really need).

The pain shows up at different times:

  • With upper bounds: more frequent "the resolver can't satisfy this" errors, more frequent need to bump the pin manually.
  • Without upper bounds: silent acceptance of breaking changes during routine upgrade runs, with the failure mode being broken builds days or weeks later when a transitive bump turns out to have removed something.

uv currently sits in the without-upper-bounds camp by default and offers --bounds major as a preview opt-in. The argument in loopwerk-uv-ux-mess is that the unsafe direction is the wrong default โ€” most users won't audit lockfile diffs carefully enough to catch a major-version bump landing through a transitive dependency, and the cost of an over-tight constraint (a quick manual bump) is much cheaper than the cost of a silent break (an outage you have to bisect).

This is also a story about who absorbs the cost. Library publishers prefer no upper bounds on their dependencies because it avoids the version-floor explosion problem. Application authors prefer upper bounds because they're the ones who have to deal with the broken builds. Default constraint behavior is the package manager taking a side in that conflict.

See also