uv sync removed a manually installed package: preserve it or declare it?
By ON Lab · Sources checked November 7, 2026
uv sync uses exact syncing by default: packages not selected for installation can be removed from the project environment. If a separate installer put a package into .venv, a successful sync can therefore remove it. Use --inexact to retain extraneous packages for that invocation, or declare a required dependency so the project can reproduce it.
Those choices solve different problems. Retaining today's environment can help an interactive experiment. Recording the dependency is what lets another checkout install the same requirement. Neither choice proves that a package is safe, licensed for your use, or compatible with the rest of the environment.
Confirm removal before reinstalling repeatedly
Record the exact command, selected interpreter, virtual environment, and first output that lists an uninstall. A missing import is not sufficient evidence of removal: an IDE, notebook kernel, terminal, or subprocess can be using a different Python. Inspect the environment you actually synced instead of repeatedly running a different system pip.
uv pip show package-name
uv sync --inexact
uv pip show package-name
Replace the placeholder with the distribution name, which can differ from the Python import name. Read the displayed location as well as the version. The commands above are a diagnostic sequence, not a promise that the package was already present. Inexact syncing retains extraneous packages; it does not fetch an undeclared package that has already disappeared.
The concrete reader problem
In the linked uv issue, a user installed spaCy model wheels through a separate download command. Those distributions were not recorded in pyproject.toml, and the next sync reported removing them. The user already knew about inexact syncing and wanted to preserve only specified additional packages while retaining automatic cleanup.
The issue's maintainer reply described inexact syncing as the available option at that time. Treat that as historical context, not a complete current configuration reference. The current official sync documentation still describes exact sync and the inexact flag. For a permanent requirement, represent the dependency in the project's supported metadata rather than assuming a one-time install changed that metadata.
Our offline reproduction and its limits
We ran uv 0.13.7 on macOS arm64 with the installed Python 3.9.6. A disposable project had no declared dependencies and no build system. We created a tiny original wheel named local-probe 0.1.0 containing one Python module; it contacted no package index and ran no external project's installer.
- Initial offline sync created the project's environment.
- A separate offline uv pip install installed the local wheel.
- uv pip show found local-probe.
- Inexact offline sync completed, and uv pip show still found it.
- Default offline sync completed and reported uninstalling it.
- uv pip show then exited 1 because the package was absent.
The sync commands both exited 0. A successful environment reconciliation can intentionally remove an unmanaged package; an exit code alone cannot tell you whether that removal matches your intended workflow. This reproduction isolates package selection. It does not test spaCy model downloads, platform-specific wheels, private indexes, or your application's imports.
Repeat the comparison in a disposable environment
Use a new directory and an installed compatible Python interpreter. Save this metadata as pyproject.toml. The example avoids installing the project itself so no build backend is needed.
[project]
name = "sync-check-example"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = []
uv sync --offline --no-python-downloads --no-install-project
uv pip install --offline /absolute/path/to/local_probe-0.1.0-py3-none-any.whl
uv pip show local-probe
uv sync --inexact --offline --no-python-downloads --no-install-project
uv pip show local-probe
uv sync --offline --no-python-downloads --no-install-project
uv pip show local-probe
The wheel path is a prerequisite, not a downloadable artifact supplied by this page. Use only a wheel you control and have reviewed. We used an original fixture whose module only defines a constant, and explicitly selected /usr/bin/python3 for initial sync. If Python cannot be found, select an installed interpreter rather than enabling downloads unintentionally. Run each command separately and capture stdout, stderr, and its exit code.
Choose the durable fix by purpose
| Purpose | Approach | Completion check |
|---|---|---|
| Required application package | Declare it with the appropriate uv dependency workflow | Review metadata and lockfile; test a clean environment |
| Optional application feature | Declare an optional dependency and select the matching extra | Sync with that extra and test its actual import or feature |
| Development-only tool | Use an appropriate dependency group | Confirm the group is selected in the target workflow |
| Temporary unmanaged experiment | Use inexact syncing deliberately | Confirm package presence; record that it is not reproduced from project metadata |
uv add package-name is suitable when that distribution belongs in the project's dependencies. Inspect the resulting metadata and lockfile changes before committing. Do not blindly add every package from an existing environment: some are transitive dependencies or leftovers. A direct URL or local wheel also needs a valid source declaration and a deployment plan; a workstation-only path will not become portable by being added to a file.
Extras, groups, and the misleading word “all”
Optional dependencies are defined under project.optional-dependencies, and uv does not sync extras by default. Use the actual extra name with --extra or the documented --all-extras option for all extras. An extra literally named all is not a universal instruction to install every package.
Development groups are separate. The dev group is selected by default under the documented default behavior, while other groups and exclusions have their own flags. A package can be declared yet not selected for a particular sync. Check selection before concluding that uv forgot the dependency.
Inexact sync is not a clean-environment guarantee
Inexact syncing intentionally leaves extra state behind. A test can pass because of a package that a clean checkout will not install. Before handing off a project or changing CI, run the intended installation in a new environment and test the actual application. Keep that verification separate from preserving an interactive notebook environment.
The official documentation also distinguishes uv run: it uses inexact syncing by default, while uv run --exact requests cleanup. Do not assume an experiment with uv run proves the behavior of uv sync, or that a subsequent exact command will keep the same unmanaged state.
What if --locked succeeds but the package is still missing?
Lockfile freshness and package selection are different checks. A fresh lockfile can still omit an undeclared distribution, and an unselected extra may not be installed. The locked-versus-frozen metadata experiment addresses freshness; it does not make an unmanaged package a dependency.
Keep the original uninstall output, package name, environment path, uv version, selected extras and groups, and metadata diff. If those do not explain the result, a small reproducible public project is more useful than a full private environment dump. Exclude credentials and private index addresses from shared logs.
Checked November 7, 2026. The offline fixture comparison was actually executed; application compatibility, third-party wheel safety, and a fresh installation of a real application were not tested.