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 | brew install uv # macOS |
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 | uv init text-classifier |
The generated structure:
1 | text-classifier/ |
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 | # 添加依赖(不指定版本 — uv 自动选最新兼容版本) |
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 | [project] |
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 | [project] |
The difference between the two lies in the scenarios in which they are installed:
1 | uv sync # 装 dependencies + dev dependencies(默认,本地开发用) |
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 | uv tree # 查看完整依赖树 |
Upgrade a package to the latest compatible version:
1 | uv lock --upgrade-package numpy # 只升级 numpy |
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 | uv run python train.py # 运行训练脚本 |
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 | uv python install 3.12 # 安装指定 Python 版本 |
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 | uv tool install ruff # 全局安装,自动隔离环境 |
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 | # 导出 conda 环境中的 pip 依赖 |
You can also use conda to export environment.yml:
1 | conda env export > environment.yml |
2. Initialize the uv project and import dependencies
1 | uv init # 如果还没有 pyproject.toml |
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 cmakeon macOS,apt install build-essentialon 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 | my-project/ |
After migration:
1 | # 用 conda 装系统级依赖 |
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.