uv for Python Projects: Clear Workflows and What to Avoid

When I started learning about uv, I was confused. The docs were solid but there was no guidance on best practices or common pitfalls or what not to do.
Look at these commands:
uv init
uv venv
source .venv/bin/activate
uv add polars marimo
uv pip install polars marimo
pip install polars marimo
uv run hello.py
uv python hello.py
python hello.py
What’s going on here? Can I call uv init
in an existing project? When should I call uv venv
? Why are there so many different ways to install and run a package?
First off: uv python hello.py
isn’t even a valid command. (This alone shows how confusing the interface can seem at first.)
But more broadly, here’s my understanding of how to properly use uv. When working on a Python project, you should pick one of two valid workflows.
Workflow 1: Use uv’s High-Level Project API (the full uv experience)
You should use this workflow when possible. The uv “high-level APIs” aka uv “project APIs” cover both Python execution and package management duties. They automatically handle virtual environment creation (no need to run uv venv
), dependency management, and package locking/syncing processes. The common commands are:
uv init # For creating a new Python project
uv add package # Install package
uv add -r requirements.txt # If you're migrating from a requirements.txt file
uv remove package # Uninstall package
uv run main.py # Run main.py
uv sync # Sync the project's dependencies with the environment
uv lock # Create a lockfile for the project's dependencies
uv lock --upgrade-package package # Update package to latest compatible version
The project APIs are opinionated (e.g. you must use pyproject.toml
). So, you should only use these commands when working in a project where uv is already the explicit package manager of choice.
This means, if you’re working on an existing project, Workflow 1 may not be the move.
Workflow 2: Use uv’s Low-Level API
If you can’t use the uv high-level API, you can use the more flexible low-level API. The workflow is as follows. First, use uv to create the virtual environment. Then, use this virtual environment as you typically would use any .venv
environment — simply activate and use vanilla Python commands. For example:
uv venv --python 3.11
source .venv/bin/activate
which python # ./.venv/bin/python
python hello.py
The benefits of this approach are:
This workflow is super compatible with existing projects
uv lets you easily specify your project’s Python version
One important caveat: Unlike the .venv
created by the standard python3 -m venv .venv
, the .venv
created by uv venv
does not install pip within that environment by default. This is because uv recommends you use uv pip
instead of pip
. So the full workflow looks like:
uv venv --python 3.11
source .venv/bin/activate
which python # ./.venv/bin/python
python hello.py
pip install package # ❌ Error: command not found: pip
uv pip install package # ✅
uv pip remove package # ✅
For completeness, it’s worth mentioning that you don’t technically need the virtual environment to be active in order for uv pip
to work correctly. uv pip install package
always respects your .venv
and fails if none exists. Hence it is safer than pip install
, which might install globally if you forget to activate your environment.
TL;DR
Use uv’s high-level API when possible. Otherwise, use the low-level API (uv venv
, source .venv/bin/activate
, uv pip
, and plain old python file.py
).
Sources
Subscribe to my newsletter
Read articles from Ishaan Jain directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
