Give two different tools the same requirements.in and you might get different lockfiles. Both correct. This is not a bug.
Dependency resolution sounds like simple math. You said flask>=2.0, I find a version that satisfies that, done. But it’s actually a constraint satisfaction problem — and constraint satisfaction problems have multiple valid solutions. The tools just disagree on which one to pick.
Two families of solvers
Backtracking is the old guard. pip and pip-tools use it. The algorithm picks a version for a package, fetches its dependencies, and keeps going. When it hits a conflict — say, package A wants werkzeug<3.0 but package B wants werkzeug>=3.1 — it backtracks. Undo the last choice, try the next candidate.
This works. It can also be brutally slow. Deep dependency trees with lots of conflicts turn into a combinatorial mess. And the order you try candidates in shapes the final result.
PubGrub is the newer approach. uv and poetry use it. Instead of blindly backtracking, it does something clever — when it hits a conflict, it learns from it. It derives a new constraint (“these two ranges are incompatible”) and propagates that knowledge forward. Conflict-driven solving. Much faster in practice.
Both families are order-dependent in version selection. They iterate through candidates in some order, and the first valid solution wins. Different iteration order, different valid solution.
Then there’s conda, which uses a SAT solver (libsolv). Entirely different ecosystem — it encodes the whole problem as boolean satisfiability and throws a solver at it. Elegant, but conda has its own problems.
Same input, different output
So why does uv pip compile give you a different lockfile than pip-tools?
Multiple valid solutions exist. flask>=2.0 could resolve to 2.3.0 or 3.1.0 — both satisfy the constraint. The solver has to pick one, and “pick one” means applying a strategy. Latest version? First version found in the index? Lowest version that satisfies?
Different tools make different choices here. And then the cascading effects kick in — a different Flask version pulls different transitive dependencies, which pull different versions of their dependencies, and you end up in a completely different corner of the solution space.
There are subtler differences too. Metadata interpretation edge cases. How markers like python_version and sys_platform get evaluated. Whether the tool considers yanked versions. It adds up.
Determinism (sort of)
Within one tool, you do get determinism — given the same input and the same index state. Same requirements.in, same PyPI, same Python version, same platform. You’ll get the same lockfile.
But PyPI is mutable. Someone publishes werkzeug==3.2.0 on Tuesday, and now your Wednesday resolve picks it up. Same input file, different result. The input to the solver changed — you just couldn’t see it.
The lockfile escape hatch
This is why lockfiles exist. Not as a convenience. As the only mechanism that gives you reproducibility.
The lockfile captures one valid solution out of potentially thousands. It pins exact versions, records hashes, freezes the entire dependency graph at a point in time. Your resolver can disagree with my resolver about what the “right” answer is — doesn’t matter, because neither of them is resolving anything in production. The lockfile already has the answer.
requirements.txt (when fully pinned) is a lockfile. uv.lock is a lockfile. poetry.lock is a lockfile. The format differs. The function is identical: stop resolving, start installing.
Bazel’s radical position
Bazel takes this to its logical extreme — it refuses to resolve at all.
The reasoning is pure Bazel: resolution is non-deterministic, non-determinism violates hermeticity, and hermeticity is non-negotiable. So Bazel consumes lockfiles but doesn’t produce them. You resolve somewhere else, hand Bazel the pinned result, and it builds from that.
It’s the most honest position in the ecosystem. Every other tool pretends resolution is a solved problem. Bazel just says: “I don’t trust it, give me the answer.”
The takeaway
Resolution is a constraint satisfaction problem with multiple valid solutions. Your tool picks one. A different tool picks a different one. Both are correct.
If that makes you uncomfortable — good. Commit your lockfile.