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
pip
andpip-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.lock
file, 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;uv
handles it for you seamlessly. - Composable: Despite its all-in-one nature,
uv
is built on and respects Python standards like PEP 517/518 (pyproject.toml
). It’s a drop-in replacement forpip
andpip-tools
and 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.,requests
orpytest
). 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 sync
will create the exact same environment every time, on any machine.- Virtual Environment (
.venv
): This is the isolated directory whereuv
installs all the packages listed in the lockfile.uv
creates and manages this environment for you, and itsrun
command 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 init
in your project root. - Import Dependencies: If you have a
requirements.txt
file, 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.txt
withuv sync
. - Replace
python -m pytest
withuv 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 tellinguv
that<package>
is a dependency of your project.uv
will:- Add the package to your
pyproject.toml
. - Resolve the full dependency tree.
- Update the
uv.lock
file with the exact versions. - Install the package into your
.venv
.
Use
uv add
for 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.toml
oruv.lock
files. 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