How uv Works: Python Environments, Dependency Resolution, and Caching
uv is a fast Python package and project manager written in Rust. It coordinates Python version selection, virtual environments, dependency resolution, package downloads, caching, lockfiles, and command execution. It does not replace the Python interpreter; it prepares and manages the environment in which Python runs.
Declare requirements, resolve a dependency graph, record the result, reuse cached artifacts, synchronize an isolated environment, and execute the command with the correct Python.
The overall workflow
pyproject.toml / requirements.txt
|
v
Project discovery and configuration
|
v
Dependency resolution
|
v
uv.lock
|
v
Global package cache
|
v
Virtual environment
|
v
uv run command
1. Selecting Python
Before installing packages, uv selects a suitable Python interpreter. It can discover an existing interpreter or install a requested version:
uv python install 3.12
uv python list
uv venv --python 3.12
Project requirements such as requires-python = ">=3.12" help uv choose a compatible interpreter. This is broader than the traditional pip model, which normally assumes that Python and the target environment already exist.
2. Building the dependency graph
A project rarely depends only on the packages listed directly by the developer. For example, an application may depend on fastapi, which in turn depends on other packages. uv reads package metadata and builds the complete graph of direct and transitive dependencies.
Dependencies can come from pyproject.toml, a legacy requirements.txt, a private package index, a local directory, a URL, or a Git repository. The pip-compatible workflow remains available:
uv pip install -r requirements.txt
3. Resolving versions with PubGrub
uv uses PubGrub, through the Rust implementation pubgrub-rs, to solve version constraints. The resolver starts with direct requirements, reads the constraints of candidate versions, propagates those constraints through the graph, and backtracks when necessary.
Suppose one package requires package-c < 3 while another requires package-c >= 3. There is no valid solution, and the resolver can explain the conflicting constraints instead of merely reporting a generic installation failure.
This incremental, constraint-based approach is one reason uv can resolve large dependency graphs efficiently. The exact result still depends on package metadata, indexes, platform markers, Python versions, and available distributions.
4. Cross-platform resolution
Python dependencies can vary by platform:
pywin32; sys_platform == "win32"
uvloop; sys_platform != "win32"
uv can represent platform- and interpreter-specific outcomes in the project lockfile. This allows Windows, Linux, macOS, and different Python versions to use the appropriate artifacts. A universal lockfile does not make application code or native system dependencies automatically portable, so platform testing is still necessary.
5. Locking with uv.lock
The project configuration describes what the project wants; the lockfile records what the resolver selected:
| File | Role |
|---|---|
pyproject.toml |
Project metadata and direct dependency declarations |
uv.lock |
Resolved dependency graph and exact package choices |
.venv |
The local project runtime environment |
requirements.txt |
Legacy pip-compatible installation input |
Generate or update the lockfile with:
uv lock
In CI, uv sync --locked can require the lockfile to already match the project configuration, preventing an unexpected re-resolution during a build.
6. Global caching and installation
uv maintains a global cache of package metadata and downloaded artifacts. When multiple projects need the same wheel, later installations can reuse the cached file instead of downloading it again. On supported filesystems, uv may use hard links or copy-on-write techniques to reduce duplication; otherwise it can fall back to copying.
The cache is not the same as .venv. The cache is a shared artifact store, while .venv is the environment used by one project.
7. What uv run does
When you run:
uv run pytest
uv run python main.py
uv finds the project, reads its configuration and lockfile, checks or synchronizes the environment, selects the project's Python executable, and launches the requested command. You do not necessarily need to activate .venv manually.
In that sense, uv run pytest is conceptually similar to:
source .venv/bin/activate
pytest
but makes environment selection explicit and is convenient in CI and scripts.
8. uv pip versus project mode
| Command | Meaning |
|---|---|
uv pip install -r requirements.txt |
Install these requirements into a selected environment |
uv sync |
Make the project environment match pyproject.toml and uv.lock |
uvx ruff check . |
Run a CLI tool in an isolated tool environment |
This distinction makes uv suitable for gradual migration: an old project can begin with uv pip and later adopt the full pyproject.toml + uv.lock workflow.
Why uv is fast
uv's performance comes from several mechanisms working together: a Rust implementation, an incremental resolver, shared caching, parallel downloads, efficient installation, and lockfiles that avoid repeating version selection when they are valid. Actual performance depends on network conditions, cache hits, dependency size, filesystem behavior, and whether packages must be compiled from source.
Conclusion
uv is best understood as an environment and project orchestrator for Python. It does not execute Python code itself. Instead, it selects or installs Python, resolves compatible dependencies, records reproducible results, populates an isolated environment, and starts the normal Python interpreter with the right packages available.
For further details, see the official uv documentation, the resolver internals guide, and the caching documentation.