Desk Clarity

Everyday document & browser help

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.

  1. Initial offline sync created the project's environment.
  2. A separate offline uv pip install installed the local wheel.
  3. uv pip show found local-probe.
  4. Inexact offline sync completed, and uv pip show still found it.
  5. Default offline sync completed and reported uninstalling it.
  6. 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

PurposeApproachCompletion check
Required application packageDeclare it with the appropriate uv dependency workflowReview metadata and lockfile; test a clean environment
Optional application featureDeclare an optional dependency and select the matching extraSync with that extra and test its actual import or feature
Development-only toolUse an appropriate dependency groupConfirm the group is selected in the target workflow
Temporary unmanaged experimentUse inexact syncing deliberatelyConfirm 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.

Sources