Skip to main content

uv: Python Project Environments and Dependencies

For an ordinary repository whose dependencies are available through Python package indexes and suitable wheels, a clear default is:

uv + pyproject.toml + uv.lock + repository-local .venv

This does not make Conda obsolete. uv primarily owns Python projects, Python versions, dependency resolution, and command execution. Conda can also own native libraries, non-Python executables, and cross-language environments. Give each environment one explicit owner; do not let pip, uv, and Conda alternately mutate it.

Choose by Boundary

ConditionStarting choiceReason
Application, CLI, script, or service with suitable wheelsuvdeclaration, lock, environment, and commands stay project-local
Python extensions have target-platform wheelstest a uv shadow environmentinstallability does not prove runtime parity
CUDA, BLAS, GDAL, GEOS, TA-Lib, or another native tool must come from Condaretain Condauv does not replace system or cross-language dependencies
Production automation names a Conda environmentretain the reference and validate in parallelmigration must not silently change the runtime path
Sources, licenses, or target platforms are uncleardeferfirst establish inventory and acceptance criteria

A long conda list is not a direct-dependency list. It contains transitive packages, native runtimes, and Conda infrastructure. Reconstruct intent from pyproject.toml, requirements, source imports, entry points, tests, and deployment commands.

Project Model

pyproject.toml intended Python range and direct dependencies
uv.lock exact dependency graph resolved by uv
.python-version optional project Python selection
.venv/ local installation; ignored by Git

Commit uv.lock; do not edit it manually. A lock captures resolution, not trust, safety, licensing, or deployment fitness.

Start a Project

uv python install 3.12
uv init example-app
cd example-app
uv python pin 3.12

uv add requests
uv add --dev pytest
uv sync
uv run python -m pytest

For an internal script collection that is not installed as a package:

[project]
name = "example-tool"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []

[tool.uv]
package = false

package = false changes installation behavior; it does not remove the need for tests, versions, and dependency declarations.

Adopt an Existing Repository

  1. Read existing project metadata, requirements, entry points, CI, and production launch paths.
  2. Declare only dependencies directly required by code and runtime paths; do not copy an installed environment wholesale.
  3. If pyproject.toml exists, start with uv lock and uv sync; migrate requirements against the official guide.
  4. Keep the old environment and build a repository-local .venv as a shadow.
  5. Run compilation, unit/integration tests, and representative read-only workloads.
  6. For numerical or data workloads, compare results, backends, threading, and performance—not only imports.
  7. Switch CI, schedulers, services, or editors last, with an explicit rollback target.

Do not combine an environment-manager migration with a Python upgrade, broad dependency refresh, or application refactor; that destroys causal diagnosis.

Everyday Commands

# Restore and execute
uv sync
uv run python --version
uv run python script.py
uv run python -m unittest discover -s tests
uv pip check

# Change declarations deliberately
uv add pandas
uv add --dev pytest
uv remove pandas
uv tree

# Require an unchanged lock
uv lock --check
uv sync --locked
uv run --locked python -m pytest

# Run a tool without adding it to project dependencies
uvx ruff check .

By default, uv sync updates the lock when project metadata requires it. CI and release paths can use --locked so stale metadata fails instead of silently producing another resolution. --frozen uses the existing lock without checking consistency; use it only when that distinction is intentional.

Prefer uv add/remove over manual installation into .venv. The environment is disposable; pyproject.toml and uv.lock hold project state.

Coexist with Conda

Coexistence should happen at the repository level, not through two resolvers owning one environment:

  • one project may use uv while another retains Conda;
  • one project may temporarily keep a Conda reference beside an independent uv shadow;
  • if Conda owns a native runtime, document the ownership and complete command path;
  • successful uv sync is not permission to delete the reference environment or change production.

Predeclare cutover criteria: tests pass, representative outputs agree, dependency checks are clean, target-platform installation succeeds, runtime behavior is verified, and rollback is rehearsed.

Common Failures

SymptomInspect
local success but CI re-resolvescommitted uv.lock and --locked use
unexpected Python under uv run.python-version, requires-python, shell environment
installation succeeds but native behavior changeswheel backend, BLAS/CUDA, system libraries, threads
works only after manual activationlaunch command fails to express its environment
large lock filenormal; distinguish direct declaration from transitive resolution
private-index authentication failscontrolled credentials; never put tokens in docs, URLs, or Git

Privacy Boundary for Public Notes

Public examples use example-app, /path/to/repository, and placeholder services. Repository names, internal domains, usernames, absolute directories, service units, migration status, test counts, and unpublished dependencies belong in private operational records. A public tool guide should preserve reusable decisions and verification methods, not publish a workspace asset inventory.