UV Getting Started Notes

This page is machine-translated and may contain errors. Read the Chinese original

Why I Started Using uv

Some open-source projects I've recently been involved with have started using uv as their package manager, and there's a uv.lock next to every pyproject.toml. I'd always used conda to manage Python environments before — I have to say conda is genuinely handy for installing non-Python binary dependencies (like cudatoolkit) — but every time I ran conda install I had to wait for dependency resolution until I questioned my life choices, and a single environment could easily take up several gigabytes, not to mention the occasional weird version conflicts.

So I spent an afternoon going through uv's documentation, sorted out its common workflows, and wrote them down here. I'll also compare it with conda later on, to make it easier to decide which tool to use in which scenario.

This article is based on the official uv documentation uv docs and the official conda documentation conda docs. The uv version at the time of writing is 0.8.x. Some content draws on the introductory blog post published by the Astral team uv: The Next-Generation Python Package Manager.

What is uv

uv is a Python environment and package manager written in Rust by the Astral team (also the developers of ruff). Its core selling point is just one thing: speed. Dependency resolution and downloads are an order of magnitude faster than pip.

Its goal is to unify the various tools in the Python toolchain — pip, venv, pyenv, pipx, poetry, and so on — all merged into a single binary. According to the official documentation, uv can be used either as a single tool to replace the pip workflow (uv pip install), or as a complete project management tool (uv init + uv add + uv sync).

Installation

One command (macOS / Linux):

1
curl -LsSf https://astral.sh/uv/install.sh | sh

Windows:

1
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

You can also use a package manager:

1
2
brew install uv      # macOS
scoop install uv # Windows

For specific installation methods, refer to the official installation documentation Installing uv.

After installation:

1
uv --version

It's just a single binary, under 30MB, with no dependency on any Python runtime.

Common Workflows

uv's project concept revolves around pyproject.toml and uv.lock. Below, we use a text classifier project text-classifier as an example to demonstrate the complete uv workflow.

Creating a Project

1
2
uv init text-classifier
cd text-classifier

The generated structure:

1
2
3
4
5
6
text-classifier/
├── .python-version
├── pyproject.toml
├── README.md
├── uv.lock
└── hello.py

pyproject.toml follows the standard PEP 621 format and is not uv-specific — meaning that even if you stop using uv later, the project definition remains portable. For more options on project initialization (such as specifying a Python version, creating an app vs. a library project), see the official project documentation.

Managing Dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 添加依赖(不指定版本 — uv 自动选最新兼容版本)
uv add numpy pandas scikit-learn

# 精确控制版本号
uv add "numpy>=1.24,<2.0" # 版本范围
uv add "scikit-learn==1.5.0" # 锁定精确版本
uv add "torch~=2.3.0" # 兼容版本(>=2.3.0, ==2.3.*)
uv add "pandas>=2.0" # 最低版本

# 开发依赖
uv add --dev pytest black ruff

# 移除依赖
uv remove numpy

Version specifier syntax follows the PEP 508 specification. Commonly used version operators:

Operator Meaning Example
>= not lower than "numpy>=1.24" — 1.24 and above
<= not higher than "numpy<=2.0" — 2.0 and below
== exact version "torch==2.3.0" — only 2.3.0
~= compatible version "pandas~=2.0" — >=2.0, <2.1
!= exclude version "numpy!=1.24.0" — skip buggy versions
>, < strict size "numpy<2.0" — 1.x series

They can be combined: uv add "numpy>=1.24,<2.0,!=1.24.3".

After each uv add, uv automatically updates pyproject.toml and uv.lock. After adding dependencies to text-classifier, the dependencies section of pyproject.toml becomes:

1
2
3
4
5
6
7
8
9
[project]
name = "text-classifier"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"numpy>=1.24,<2.0",
"pandas>=2.0",
"scikit-learn==1.5.0",
]

Development dependencies added with uv add --dev are written to [dependency-groups].dev, separate from runtime dependencies. After adding pytest / black / ruff, the two dependency sections of pyproject.toml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[project]
name = "text-classifier"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"numpy>=1.24,<2.0",
"pandas>=2.0",
"scikit-learn==1.5.0",
]

[dependency-groups]
dev = [
"pytest>=8.0",
"black>=24.0",
"ruff>=0.5",
]

The difference between the two lies in the scenarios in which they are installed:

1
2
uv sync           # 装 dependencies + dev dependencies(默认,本地开发用)
uv sync --no-dev # 只装 dependencies,跳过 pytest/black/ruff(CI 用)

In CI, using --no-dev saves unnecessary time and space; locally, just run uv sync to install everything.

uv.lock records the resolved exact versions (including hashes of all transitive dependencies). To see the currently locked version of a package:

1
2
uv tree                    # 查看完整依赖树
uv tree --depth 1 # 只看直接依赖

Upgrade a package to the latest compatible version:

1
2
uv lock --upgrade-package numpy   # 只升级 numpy
uv lock --upgrade # 升级全部

Syncing the Environment

1
uv sync

Reads uv.lock and precisely syncs the packages in the environment to the locked versions. After someone else pulls the code, running uv sync once gives them an exactly identical environment.

Running Commands

1
2
3
uv run python train.py         # 运行训练脚本
uv run python -c "import sklearn; print(sklearn.__version__)"
uv run pytest # 跑测试

No need to activate the venv first. uv run automatically uses the .venv under the project directory, which is convenient when writing Makefiles or CI scripts.

Python Version Management

uv has built-in Python version management similar to pyenv (see Python versions for details):

1
2
3
uv python install 3.12    # 安装指定 Python 版本
uv python list # 查看可安装的版本
uv python pin 3.12 # 为当前项目锁定版本

No need to manually install Python in advance — uv downloads it automatically. The .python-version file and the requires-python in pyproject.toml together determine which Python to use.

Tool Management

Some Python CLI tools (such as ruff, pre-commit) don't need to be bundled into project dependencies. uv provides a tool management mode similar to pipx (Using tools):

1
2
3
uv tool install ruff       # 全局安装,自动隔离环境
uv tool list # 查看已装工具
uvx ruff check . # 不安装直接运行(uvx = uv tool run)

uv vs. conda

I used conda for a long time, and after switching to uv my biggest impression is that it's much faster, but also missing a few things. Below is a comparison across several dimensions.

Install Speed and Dependency Resolution

This is uv's biggest advantage. The Rust-based resolver, combined with a global cache and parallel downloads, makes everyday uv add and uv sync essentially instantaneous. conda's dependency resolution (especially with the default channel) is notoriously slow — adding a single package can sometimes take minutes to solve.

uv's official claims it's 10–100x faster than pip. My own experience on my machine: for a medium-sized deep learning project, rebuilding the environment from environment.yml takes about 3–5 minutes with conda, while uv rebuilds from uv.lock in under 30 seconds.

Dependency Management

uv conda
Dependency declaration pyproject.toml (PEP 621 standard) environment.yml or conda.ini
Lock file uv.lock (cross-platform exact locking) No built-in lock file mechanism
Package source PyPI (Python packages) conda-forge / defaults (Python + non-Python packages)
Dependency resolver Rust implementation, fast SAT solver based, slow

uv's uv.lock is a cross-platform exact lock file, something conda has no built-in equivalent for. conda's environment.yml can specify version ranges, but exact reproduction usually requires additionally exporting conda list --export.

Environment Isolation

conda environments are global — all environments are managed under a single directory (viewable with conda info --envs). The upside is easy switching; the downside is that environments from different projects easily get mixed up.

uv environments are created by default in the project directory under .venv, naturally bound to the project. This design is similar to npm's node_modules — the project and the environment are one unit. I prefer this approach: when you delete a project, the environment goes with it, leaving no orphaned environments behind.

Non-Python Dependencies

This is conda's real moat. conda can manage any binary package — Python, R, C libraries, CUDA toolkit, and so on. For example, installing PyTorch + CUDA:

1
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia

uv can only manage Python packages. If you need system-level dependencies like ffmpeg, cmake, or cuda-toolkit, you either use a system package manager (apt/brew) or keep pairing uv with conda. This is currently uv's biggest limitation.

Python Version Management

conda lets you specify the Python version directly when creating an environment:

1
conda create -n myenv python=3.11

uv achieves similar functionality through uv python install + uv python pin. The two are not very different in practice; uv automatically downloads the corresponding Python version on first use.

Use Case Recommendations

Scenario Recommended tool Reason
Pure Python projects (web, ML training scripts, etc.) uv Fast, lightweight, uv.lock guarantees reproducibility
Needs CUDA / non-Python binary dependencies conda (or conda + uv mixed) conda's binary package ecosystem is irreplaceable
Open source projects / team collaboration uv pyproject.toml + uv.lock are cross-platform and CI-friendly
Data science / rapid experimentation conda The conda-forge ecosystem is mature; one line installs the whole stack
Long-term maintenance of existing conda projects Keep as-is / migrate gradually Don't fix what isn't broken, unless a pain point is pushing you

My current strategy: new projects prefer uv; when CUDA or packages only available on conda-forge are involved, I keep using conda. Sometimes I mix the two — using conda for system-level dependencies and CUDA, and uv for managing Python packages.

Migrating from conda to uv

If you already have a conda-managed project, migrating to uv takes roughly three steps. For a detailed migration guide, refer to uv's official Migration Guide.

1. Export the current environment dependencies

1
2
3
4
5
# 导出 conda 环境中的 pip 依赖
conda list --export | grep -v "^#" | cut -d " " -f 1 > requirements.txt

# 或者只导出 pip 安装的包
pip freeze > requirements.txt

You can also use conda to export environment.yml:

1
conda env export > environment.yml

2. Initialize the uv project and import dependencies

1
2
3
4
uv init                    # 如果还没有 pyproject.toml
uv add -r requirements.txt # 导入 pip 格式的依赖列表
uv lock # 生成 uv.lock
uv sync # 同步环境

3. Handle non-Python dependencies

Non-Python packages installed via conda (cuda-toolkit, gcc, cmake, etc.) are outside uv's scope. There are several ways to deal with them:

  • Keep using conda for system dependencies: conda install cuda-toolkit cmake, and hand all Python packages over to uv. This approach doesn't actually conflict — just make sure the Python in the conda environment and the one uv uses are the same version.
  • Use the system package manager: brew install cmake on macOS, apt install build-essential on Ubuntu.
  • Use Docker: bake system dependencies into the image, and install Python dependencies with uv inside the image.

I personally prefer the third option (Docker) — first, the environment is cleanest, and second, it's easy to reproduce in CI. But if it's just local development, mixing conda + uv is fine too — conda is just a mover of binary files, and it doesn't conflict with pip/uv.

Complete migration example

Suppose the original project structure is:

1
2
3
my-project/
├── environment.yml # conda 环境定义
└── src/...

After migration:

1
2
3
4
5
6
7
8
9
10
# 用 conda 装系统级依赖
conda create -n sys-deps cuda-toolkit cmake gcc -y

# 用 uv 管理 Python 项目
cd my-project
uv init
uv add torch numpy pandas
uv add --dev pytest
uv sync
uv run python src/main.py

Summary

uv isn't anything revolutionary — it does the same things pip / conda do: install packages, manage environments, lock versions. But it does these things faster and more unified.

For me personally, the biggest gains are two things: (1) no more waiting for dependency resolution — uv add is basically instantaneous; (2) uv.lock gives project dependency reproducibility a clear mechanism, unlike conda's environment.yml which often leads to "your version is wrong" problems.

Of course, conda is still the most convenient tool for installing non-Python dependencies, and uv can't replace that for now. But if you spend most of your time writing pure Python projects, the experience improvement from switching from conda to uv is real and tangible.