DEV Community

MilkyWay008
MilkyWay008

Posted on

uv lock says "No solution found": how to read the dependency deadlock and break it

You add one library, run uv lock, and the resolver prints 40 lines of "Because ... And because ..." and quits with a non-zero exit code. You did not change any pins. The project was fine yesterday.

I have run into this class of failure a few times and it usually ends the same way: the resolver is right, and one package in the graph has a stale upper bound.

What that wall of text is saying

The first line is the one that matters:

× No solution found when resolving dependencies:
Enter fullscreen mode Exit fullscreen mode

Everything after it is a derivation chain. Each line is a requirement read out of some package's metadata, and the chain ends where two of them want different things.

A real example, from a LangGraph issue (langchain-ai/langgraph#8352): langgraph-api 0.11.0 pins opentelemetry-exporter-prometheus<0.59, which forces opentelemetry-sdk into an old range. Meanwhile pydantic-ai 2.x pulls logfire>=4.35.0, which wants opentelemetry-sdk>=1.39.0,<1.43.0. Those two ranges never overlap, so no set of versions satisfies both.

Nobody is being unreasonable here. That 0.59 bound was probably correct when someone wrote it...... it just never got revisited. The person who filed the issue could not fix it either, because the metadata ships from a private source tree, so only the maintainers can widen the pin.

Two commands make the chain readable:

uv lock -v                            # full derivation, as the resolver sees it
uv tree --invert opentelemetry-sdk    # who wants which version
Enter fullscreen mode Exit fullscreen mode

Read the chain bottom-up, find the two packages that disagree, ignore the middle.

The silent downgrade is worse than the error

In that same issue the resolver could quietly settle on a much older langgraph-api (0.7.27 was observed), and langgraph dev fell back to an older build without complaining. The app boots, on code you did not ask for, and the Studio upgrade banner stays unsatisfiable.

That one costs people days, because nothing looks broken. A successful resolution is not proof you got what you wanted. Check the installed version, not the requested one.

Constraints cannot fix a deadlock (this is the part people get wrong)

uv has two settings for this, both in the workspace-root pyproject.toml under [tool.uv]. They look similar and behave nothing alike.

constraint-dependencies is additive. The docs describe constraints as restricting "the versions of dependencies that are selected during resolution", so they get intersected with whatever the packages already declared.

override-dependencies is absolute. The docs say overrides "force selection of a specific version of a package, regardless of the version requested by any other package, and regardless of whether choosing that version would typically constitute an invalid resolution". The resolution docs call overrides the escape hatch for erroneous upper version bounds.

So when the ranges are disjoint, a constraint can never save you. Constraints only narrow the space, and the space is already empty. This matters because the reflex is to reach for a constraint when the real tool is an override.

[tool.uv]
# stops the resolver from silently picking something ancient
constraint-dependencies = ["langgraph-api>=0.10.3"]

# the deadlock breaker: deliberately contradicts a declared upper bound
override-dependencies = ["opentelemetry-exporter-prometheus>=0.63b1"]
Enter fullscreen mode Exit fullscreen mode

One placement gotcha: uv reads these from the workspace root only. Putting them in uv.toml, or in a member package's pyproject, does nothing.

The order I would try things

1st, pin the thing that moved back to the last version that worked. Boring, safe, usually enough to get moving today.

2nd, add a floor constraint on whatever was being silently downgraded, so you find out about it instead of living with it.

3rd, override the stale bound, then actually run the app. uv lock exiting 0 only proves the graph resolves. The package that asked for the old SDK is now untested territory.

4th, if the two libraries genuinely cannot coexist, give them separate environments. Ugly, but honest, and sometimes correct.

Leave a trail

Keep the override in the file with a comment, a link to the upstream issue, and the date you added it. An override without a note turns into permanent mystery debt six months later. In this case the pin was widened on the 0.13.0 dev line (<0.64,>=0.63b1, landing the SDK at 1.42.1) while stable still carried the old bound, so the plan is: wait for the release, then delete the block.

If you use pip instead of uv, the constraint file (pip install --dry-run -c constraints.txt) is additive in the same way, so it will not break a disjoint range either. Worth knowing before spending an hour on it.

I can be wrong about your particular graph, but from what I have seen the fix is rarely a clever resolver flag. It is reading the chain, working out which bound is stale, and forcing that one thing while you wait for upstream...... then writing down why.

One honest gap: I could not independently confirm whether stable langgraph-api still carries <0.59 at the time of writing. The issue is open. Check it before copying the override.

Top comments (0)