What Are uv Overrides and Constraints?
A project depends on flask>=3.1, and Flask depends on werkzeug>=3.1. You need werkzeug pinned to 3.1.0 because 3.1.8 triggers a bug in your test suite. Adding werkzeug==3.1.0 to [project.dependencies] works, but now your project claims a direct dependency on a package it never imports.
uv provides three mechanisms for situations like this: override-dependencies, constraint-dependencies, and [tool.uv.sources]. Each one modifies dependency resolution differently, and choosing the wrong one either fails silently or breaks resolution entirely. All three live in the [tool.uv] section of pyproject.toml.
How does the resolver use version specifiers?
Before the three controls make sense, the resolver’s normal behavior needs to be clear.
When uv resolves dependencies, it collects every version specifier that mentions a given package and computes the intersection. If your project declares requests>=2.32 and requests itself declares urllib3>=1.21.1,<3, the resolver picks a urllib3 version that satisfies both its own specifiers and every other package’s requirements. When no version fits all the specifiers at once, resolution fails.
The three controls modify this process at different points. Constraints add specifiers to the intersection. Overrides replace specifiers before the intersection is computed. Sources redirect where the resolved package is fetched from.
What do constraints do?
constraint-dependencies narrows the set of acceptable versions for a transitive dependencyA package your dependency depends on. When you install requests, its own dependencies (urllib3, certifi, etc.) are transitive dependencies.
without adding it to the project’s direct dependencies.
[tool.uv]
constraint-dependencies = ["markupsafe<3"]This tells the resolver: “when anything in the dependency tree pulls in markupsafe, also require <3.” The constraint combines with the existing specifiers. If Jinja2 declares markupsafe>=2.0, the resolver now picks from >=2.0,<3 instead of >=2.0. With flask>=3.1.3 in [project.dependencies], adding this constraint pulls markupsafe from 3.0.3 down to 2.1.5:
$ uv lock
Resolved 9 packages in 1.72s
Updated markupsafe v3.0.3 -> v2.1.5
Constraints only narrow. They cannot expand a range. A constraint that conflicts with a declared specifier causes resolution to fail rather than overriding the specifier:
[project]
dependencies = ["urllib3>=1.26,<2"]
[tool.uv]
constraint-dependencies = ["urllib3>=2"]$ uv lock
error: No solution found when resolving dependencies:
Because your project depends on urllib3>=1.26,<2 and urllib3>=2,
we can conclude that your project's requirements are unsatisfiable.
The constraint >=2 combines with the direct dependency <2, producing an empty intersection. The resolver reports the conflict instead of silently ignoring the constraint.
When to reach for constraints
- Enforce version policies on transitive dependencies. A vulnerability in a transitive dependency needs a floor, but adding the package to
[project.dependencies]would misrepresent the project’s imports.constraint-dependencies = ["urllib3>=2.0.7"]sets the floor without claiming a direct dependency. The same mechanism caps a version to avoid a known-broken release:constraint-dependencies = ["markupsafe<3"]. - Align transitive dependency versions across a lockfileA file that records the exact version of every installed package, so everyone working on the project gets identical installs. . When two libraries pull in the same transitive dependency with loose specifiers, a constraint forces them to agree on a range.
What do overrides do?
override-dependencies replaces version specifiers entirely. Where a constraint adds to the intersection, an override discards the original specifiers and substitutes new ones.
[tool.uv]
override-dependencies = ["werkzeug==3.1.0"]This tells the resolver: “ignore every version specifier on werkzeug from every package in the tree, and use ==3.1.0 instead.” Flask declares werkzeug>=3.1.0, but the override replaces that with ==3.1.0, forcing exactly that version:
$ uv lock
Resolved 9 packages in 1.58s
Updated werkzeug v3.1.8 -> v3.1.0
The power of overrides is that they expand ranges, not just narrow them. When a direct dependency says urllib3<2 and an override says urllib3>=2, the override wins:
[project]
dependencies = [
"requests>=2.32",
"urllib3>=1.26,<2",
]
[tool.uv]
override-dependencies = ["urllib3>=2"]$ uv lock
Resolved 6 packages in 3.04s
Added urllib3 v2.7.0
The same configuration with constraint-dependencies instead of override-dependencies would fail, because constraints cannot override the <2 bound.
Scope overrides to specific packages
A global override applies to every package in the tree. When only one library has broken metadata, scoping the override avoids collateral damage:
[tool.uv]
override-dependencies = [
{ package = { name = "old-lib", version = "1.2.3" }, dependencies = ["pydantic>=2"] },
]This override only replaces the pydantic specifier inside old-lib==1.2.3. Every other package’s pydantic requirements stay intact. Omit the version field to target all versions of a package. Version-specific scopes take precedence over all-version scopes, which take precedence over global overrides.
When to reach for overrides
- Fix broken or incorrect version bounds. A library declares
pydantic<2but works with pydantic 2.x. The overridepydantic>=1.0,<3replaces the incorrect bound so the resolver can pick a 2.x release. The same mechanism pins a transitive dependency to a known-good version while diagnosing a regression. - Unblock resolution when two libraries disagree. If library A requires
numpy>=1.26,<2and library B requiresnumpy>=2, the resolver cannot satisfy both. An overridenumpy>=2replaces all specifiers on numpy and lets resolution proceed.
Treat overrides as a last resort
Overrides disable the safety that version specifiers provide. A library declaring werkzeug>=3.1 is promising its code works with 3.1 and later. An override that forces werkzeug==2.0 installs a version the library never tested against, and any breakage is yours to debug. Treat overrides as a temporary escape hatch. When the upstream library fixes its metadata, remove the override.
What does tool.uv.sources do?
[tool.uv.sources] changes where a package is fetched from without changing the version specifier in [project.dependencies]. The version constraint stays the same; only the source changes.
[project]
dependencies = ["flask>=3.1"]
[tool.uv.sources]
flask = { git = "https://github.com/pallets/flask", tag = "3.1.0" }$ uv lock
Resolved 9 packages in 4.20s
Updated flask v3.1.3 -> v3.1.0 (ab814966)
The lockfileA file that records the exact version of every installed package, so everyone working on the project gets identical installs.
records the git commit SHA. The [project.dependencies] entry still says flask>=3.1, so any tool that reads standard metadata (pip, Poetry, build backends) sees a normal PyPI dependency. Only uv reads the [tool.uv.sources] table.
Sources support several fetch locations:
- Git repositories:
{ git = "https://...", branch = "main" }ortagorrev - Local paths:
{ path = "../shared-lib", editable = true } - Direct URLs:
{ url = "https://files.example.com/pkg-1.0.tar.gz" } - Specific indexes:
{ index = "pytorch" }(paired with a[[tool.uv.index]]entry) - Workspace members:
{ workspace = true }
To verify that the published version on PyPI still resolves correctly, run uv lock --no-sources. This flag ignores the entire [tool.uv.sources] table and resolves against registries only:
$ uv lock --no-sources
Resolved 9 packages in 1.80s
Updated flask v3.1.0 (ab814966) -> v3.1.3
For the full range of source patterns, including marker-gated sources and switching between git and PyPI, see How to manage cross-repository Python dependencies with uv.
When to reach for sources
- Develop against unreleased or local code. Point a dependency at a git branch containing an unpublished fix, or at a local checkout so changes in a sibling project show up immediately without reinstalling.
- Pull from a specific package index. Pin a package to a private index or a specialized index like PyTorch’s CPU/CUDA wheel repositories.
How the three controls interact
The three mechanisms operate at different layers of the resolution process, so they can be combined:
| Control | What it modifies | Direction |
|---|---|---|
constraint-dependencies |
Version specifiers (additive) | Narrow only |
override-dependencies |
Version specifiers (replacement) | Narrow or expand |
[tool.uv.sources] |
Fetch location | No version effect |
A project can use all three at once. Overrides and constraints modify which versions the resolver considers; sources modify where the chosen version is fetched from.
[project]
dependencies = ["httpx>=0.27"]
[tool.uv]
constraint-dependencies = ["anyio>=4.5"]
override-dependencies = ["h11>=0.16"]
[tool.uv.sources]
httpx = { git = "https://github.com/encode/httpx", tag = "0.28.1" }The resolver applies the override on h11 (replacing all h11 specifiers with >=0.16), adds the anyio constraint (requiring >=4.5 in addition to whatever httpx declares), and fetches httpx from the git tag instead of PyPI.
Avoid these common mistakes
Using an override when a constraint would work. If the goal is to keep markupsafe below version 3, a constraint does that safely: constraint-dependencies = ["markupsafe<3"]. An override achieves the same result but also silences the error if markupsafe 2.x turns out to be incompatible with a future Jinja2 release. Prefer the tool that preserves the resolver’s ability to catch conflicts.
Expecting constraints to expand a range. A constraint that says urllib3>=2 combined with a direct dependency of urllib3<2 fails. Constraints add to the intersection; they cannot override an existing upper bound. Use override-dependencies when the existing range is wrong and needs to be replaced.
Forgetting that overrides and constraints only affect packages already in the tree. Neither mechanism adds a package to the dependency graph. override-dependencies = ["numpy==1.26"] has no effect if nothing in the project depends on numpy. The package must appear as a direct or transitive dependency for the override or constraint to activate.
Leaving overrides in place after the upstream fix ships. Overrides suppress declared version requirements. When the library that had broken metadata publishes a corrected release, the override is no longer needed, but it silently stays active. Audit override-dependencies during regular dependency maintenance.
Confusing sources with overrides. [tool.uv.sources] changes where a package is fetched from; it does not change which versions are acceptable. Pointing flask at a git tag that predates the version specifier in [project.dependencies] does not error at lock time, but the installed version may not match what the specifier promised.
Exclude a transitive dependency entirely
exclude-dependencies is a related control that removes a package from the dependency tree entirely, regardless of whether other packages request it:
[tool.uv]
exclude-dependencies = ["blinker"]$ uv lock
Resolved 8 packages in 3.26s
Flask declares blinker>=1.6.2 as a dependency, but the exclude removes blinker from the resolved tree. Flask installs without it. This is useful for stripping optional dependencies that a package declares but your application never uses.
Understand workspace scope rules
In a uv workspace, uv reads override-dependencies, constraint-dependencies, and exclude-dependencies only from the workspace root’s pyproject.toml and ignores declarations in member packages or uv.toml files. Each member’s own pyproject.toml controls its [tool.uv.sources].
Learn More
- Managing dependencies in the uv docs covers the full
[tool.uv.sources]syntax. - Resolution in the uv docs explains how the resolver handles overrides, constraints, and resolution strategies.
- How to manage cross-repository Python dependencies with uv covers git sources, editable paths, and toggling between git and PyPI.
- How to Upgrade a Dependency Past Its Version Ceiling shows how to raise a capped dependency’s upper bound and force a re-resolve.
- What is a version specifier? explains the
>=,<,~=, and==operators that overrides and constraints modify. - What is a lockfile? covers what the lockfile records after resolution.