Using uv: A Modern Python Workflow
Introduction: What is uv?
I’ve used uv on and off since it was released and have been consistently impressed by its speed. However, I still found myself confused about certain parts of its workflow, like when to use uv run versus uvx, or how it fit in with existing tools like pip and venv. I wrote this guide as a way to resolve those questions for myself, and hopefully, it can be helpful to others who are also exploring this powerful new tool.
uv is a package and project manager for Python, written in Rust by Astral (the team behind ruff). It’s designed to be an extremely fast, all-in-one replacement for pip, venv, pip-tools, and other utilities. It addresses three core areas:
- Speed: It’s orders of magnitude faster than
pipandpip-tools. - Simplicity: It offers a single command-line interface for most packaging tasks.
- Reproducibility: It encourages modern, lockfile-based workflows for deterministic builds.
The Philosophy of uv
The core philosophy behind uv is to provide “one tool to manage (almost) everything.” While it doesn’t manage Python installations (a task left to tools like pyenv or mise), it handles nearly every other aspect of your project’s lifecycle: managing dependencies, running scripts, and building and publishing packages.
uv is designed to be:
- Fast and Deterministic: It uses a global cache and a high-performance resolver to make dependency installation incredibly fast. It defaults to generating a
uv.lockfile, ensuring that every developer on your team and your CI/CD pipeline uses the exact same package versions. - Minimal Setup: Getting started is as simple as running
uv init. You no longer need to manually create and activate virtual environments;uvhandles it for you seamlessly. - Composable: Despite its all-in-one nature,
uvis built on and respects Python standards like PEP 517/518 (pyproject.toml). It’s a drop-in replacement forpipandpip-toolsand works with the existing ecosystem, not against it.
Core Concepts
uv’s workflow is built on three fundamental components that work together to create a robust and reproducible project structure.
pyproject.toml: This is the heart of your project’s configuration. It’s a standardized file where you declare your project’s metadata (like its name and version) and its direct dependencies (e.g.,requestsorpytest). You define what you need, but not the exact versions of every sub-dependency.uv.lock: This is the lockfile automatically generated and managed byuv. It contains a complete, frozen list of every package and its exact version required to run your project. This file is the key to reproducibility, as it guarantees thatuv syncwill create the exact same environment every time, on any machine.- Virtual Environment (
.venv): This is the isolated directory whereuvinstalls all the packages listed in the lockfile.uvcreates and manages this environment for you, and itsruncommand automatically uses it without requiring you to manuallysource .venv/bin/activate.
Here’s how these files compare to the traditional requirements.txt:
| File | Purpose | Managed By |
|---|---|---|
pyproject.toml | Declares direct, high-level dependencies and project metadata. | You |
requirements.txt | Traditionally lists all dependencies, often without version pinning. | You/pip |
uv.lock | Locks the entire dependency tree to specific, exact versions. | uv |
Everyday Workflows
Creating a new project
Starting a new Python project with uv is refreshingly simple.
# 1. Initialize a new project
uv init
# 2. Add a dependency
uv add requests
# 3. Run your code
uv run python main.py
This workflow replaces the traditional ceremony of python -m venv .venv, source .venv/bin/activate, and pip install .... uv automatically creates the .venv, adds requests to your pyproject.toml, updates uv.lock, and installs the package.
Adding and removing dependencies
uv makes dependency management a single-step process. When you add or remove a package, uv automatically updates both your pyproject.toml and uv.lock file, keeping everything in sync.
# Add a production dependency
uv add "fastapi[all]"
# Add a development-only dependency
uv add --dev pytest
# Remove a dependency
uv remove fastapi
Running code and tools
The uv run command executes a command within the project’s managed virtual environment, saving you from ever having to activate it manually. uv automatically detects the .venv in the current directory (or parent directories) and uses it.
# Run your main application script
uv run python src/main.py
# Run your tests with pytest
uv run pytest
# Run a linter or type checker
uv run mypy src/
Using one-off tools
Sometimes you need to run a tool without adding it as a project dependency. uvx is the perfect command for this. It creates a temporary, cached environment, installs the tool, runs it, and then discards the environment.
# Check formatting with black without installing it in your project
uvx black --check .
# Run a code formatter
uvx ruff format .
Think of uv run for tools inside your project (like pytest) and uvx for tools outside your project (like one-off scripts or formatters).
Reproducible installs
The uv sync command is the cornerstone of reproducibility. It installs the exact versions of all dependencies specified in uv.lock into your virtual environment. This is the command you and your team will use to set up the project locally and what your CI pipeline will use to build it.
# Install all dependencies from the lockfile
uv sync
# In CI, use --frozen to ensure the lockfile is up-to-date
uv sync --frozen
Using uv sync --frozen in CI will cause the build to fail if pyproject.toml has been changed without regenerating the lockfile, preventing “it works on my machine” issues.
Publishing a package
If your project is a library meant to be published, uv provides standard build and publish commands.
# Build the source distribution and wheel
uv build
# Publish the package to PyPI (or a custom index)
uv publish
Integrating uv into Existing Workflows
Migrating from pip or Poetry
Migrating an existing project to uv is straightforward.
- Initialize
uv: Runuv initin your project root. - Import Dependencies: If you have a
requirements.txtfile, you can import your dependencies directly:# Import production dependencies uv add -r requirements.txt # Import development dependencies uv add -r requirements-dev.txt --dev - Update Your Workflow:
- Replace
pip install <pkg>withuv add <pkg>. - Replace
pip install -r requirements.txtwithuv sync. - Replace
python -m pytestwithuv run pytest.
- Replace
uv add vs. uv pip install
Another point confusion for me was the difference between uv add and uv pip install. While they seem similar, they serve fundamentally different purposes.
-
uv add <package>: This is the declarative way to manage your project’s dependencies. When you useuv add, you are tellinguvthat<package>is a dependency of your project.uvwill:- Add the package to your
pyproject.toml. - Resolve the full dependency tree.
- Update the
uv.lockfile with the exact versions. - Install the package into your
.venv.
Use
uv addfor any package that is a long-term dependency of your project. - Add the package to your
-
uv pip install <package>: This is an imperative command that behaves much likepip install. It directly installs a package into the virtual environment without modifying yourpyproject.tomloruv.lockfiles. This is useful for:- Temporarily trying out a library.
- Installing a tool for a one-off task within the virtual environment.
- Situations where you need to install something without making it a formal dependency.
In short: uv add manages your project’s dependencies, while uv pip install is for imperative, one-off installations into the environment. For reproducible projects, you should almost always prefer uv add.
Working with Docker
uv can significantly speed up your Docker builds and reduce image size. By copying only the pyproject.toml and uv.lock files first, you can leverage Docker’s layer caching.
FROM python:3.11-slim
WORKDIR /app
# Install uv
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.cargo/bin:$PATH"
# Copy only the dependency files to leverage caching
COPY pyproject.toml uv.lock ./
# Install dependencies
RUN uv sync --frozen --system
# Copy the rest of the application code
COPY . .
CMD ["uv", "run", "python", "src/main.py"]
Other Questions I had
| Question | Short Answer |
|---|---|
When to use uv add vs uv pip? | Use uv add to manage project dependencies. Use uv pip for one-off tasks. |
| Do I still need virtualenvs? | Yes, but uv creates and manages them for you automatically. |
Do I need to activate .venv? | No. uv run and uvx handle environment activation for you. |
| What happens when I change directories? | uv automatically detects the nearest pyproject.toml and its .venv. |
Cheatsheet (quick reference)
Here is a quick reference for the most common uv commands.
uv init # Create a new project
uv add requests # Add a dependency
uv add --dev pytest # Add a dev dependency
uv sync # Install from lockfile
uv run pytest # Run a command in venv
uvx black . # Run a one-off tool
uv build # Build a package
uv publish # Publish a package
Additional Resources
Here are some helpful links for getting started with uv:
- Official
uvDocumentation: The complete guide touv, from getting started to advanced workflows. - Astral Blog: The announcement post for
uv, explaining the motivation and vision behind the project. - GitHub Repository: The source code for
uv, where you can report issues and contribute. - Managing Python Projects With uv: An All-in-One Solution: A comprehensive Real Python tutorial on using
uvfor the entire project lifecycle. - uv vs pip: Managing Python Packages and Dependencies: A detailed comparison of
uvandpip, highlighting the trade-offs.