UV Explained: A Faster, Simpler Python Workflow in 5 Minutes
Welcome to this article where we're going to take a look at UV, an all-in-one tool that will make working with your Python project simpler and faster. UV is a tool that handles your virtual environment, package management, dependency resolution, and Python version all in one place. It is also very fast. This basically allows it to replace a ton of other tools that you might already be using in your Python workflow.
For example, many developers have been using pip
and venv
for over a decade. But the increased speed and simplicity offered by UV is just too good to ignore, which is why it's gained such rapid adoption. And now even more so in the age of AI agents and MCP servers, where it helps to have a simple way to manage and execute Python packages.
So today, let's take a quick look at UV and how we can use it.
- First, we'll explore how to set up a project with UV and how it manages your virtual environment automatically.
- Then, we'll look at package management and how UV replaces pip
with something simpler and faster.
- Finally, we'll take a look at uvx
, a one-line command that lets you run Python tools directly without having to manually set up or install them.
Let's get started.
Getting Started: Installing UV
First, let's install UV. If you're on macOS, you can use the Homebrew package manager. If you're on Windows, you can use winget
.
On macOS:
bash
brew install uv
On Windows:
bash
winget install uv
For other platforms or different installation methods, check the official documentation for more instructions. Once it has been installed, you can verify that it's working by running this command to check the version:
uv --version
Creating Your First Project
We can build a simple API server to see how UV handles everything. You can create a project by using the init
command, followed by the name of the project. This will create the directory for you.
uv init my-fastapi-project
If you already have an existing project or an existing directory, you can also just run uv init
without having to specify the project name.
Behind the scenes, this init
command will set up a couple of important files for you inside the project directory:
-
pyproject.toml
: This is where your project configuration lives. It's similar topackage.json
or therequirements.txt
file that you might be used to. It will have all your project metadata and all of your dependencies in one place. -
README.md
: A standard README file. -
main.py
: An entry point for your project.
Once this is all set up, you can add a dependency by just running the uv add
command. For example, this line will add the FastAPI dependency to our project configuration and install it into our virtual environment.
uv add fastapi
But wait, you might have noticed that we didn't explicitly create or activate a virtual environment yet. That's exactly right. UV will create and manage this for us behind the scenes as long as we're in this project directory.
A Practical Walkthrough
Let's see this in action. First, we'll run uv --version
to confirm that it's installed. Then, we'll run the uv init
command to create a project.
uv init my-fastapi-project
This creates the my-fastapi-project
directory. Let's move into that project and list the files inside:
cd my-fastapi-project
ls
You'll see the main.py
, pyproject.toml
, and README.md
files. The initial main.py
file is simple:
# main.py
def main() -> int:
print("Hello, world!")
return 0
We can try running it by typing uv run
. The first time you run something, UV automatically creates a virtual environment for you and picks the Python version it wants to use.
uv run
Now let's add a dependency to the project:
uv add fastapi
After adding FastAPI, if you look in the project directory, you'll see that a uv.lock
file has been created. This is a very long file that contains the exact version of every dependency used in this project, ensuring deterministic builds.
Now, let's open up our project in an editor like VSCode and replace the main.py
script with a simple FastAPI server:
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
You'll notice that the dependencies all work fine. That's because modern editors like VSCode can detect the virtual environment created for us by UV.
Note: If you have any problems where you've already installed a dependency but your editor complains, just make sure that you check the Python interpreter and that it is set to the virtual environment for this project.
If you look into the pyproject.toml
file, you'll also see that the FastAPI package has been added to the dependencies list.
To run the script, let's go back to the terminal. Instead of using the python
command, we will use uv run
. You can run whatever you'd normally use Python to run, except that this will use the correct virtual environment and pull in any packages it needs.
For example, to run a FastAPI application, we'd typically use uvicorn
. We didn't explicitly install uvicorn
in this environment, but let's see what happens when we run it:
uv run uvicorn main:app --reload
No problem at all. UV figures out that it needs uvicorn
, sets it up in an isolated environment, and uses it to run our app. If you go to the provided URL (e.g., http://127.0.0.1:8000
), you will see the FastAPI server running.
As you can see, this workflow is much simpler than the traditional combination of venv
and pip
. For comparison, here's a snippet of the commands previously required:
# Old workflow
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
This old method uses two different tools and is not very intuitive. You have to create the virtual environment manually and remember to activate it every time you want to work on the project. UV significantly simplifies this part of the workflow.
Advanced Package Management with UV
Now that we've got the basics down, let's look at how UV handles package management differently from pip
. The main difference is that UV uses a proper dependency resolver and creates a deterministic lock file.
Let's add a couple of new dependencies to our project:
uv add "redis" "sqlalchemy"
UV uses the pyproject.toml
file instead of the requirements.txt
file. When we add new packages, it automatically updates this file for us, which is something pip
doesn't do for requirements.txt
. After running that command, you'll see that redis
and sqlalchemy
have been added to your configuration.
As it's doing this, UV also creates and updates the uv.lock
file. This contains the exact version of every package used in the environment, so that whenever you build this project, you get the exact same results. Without this version locking, new package versions can introduce breaking changes. Version locking prevents this and is a best practice for any production-grade software.
To check which packages have been installed, you can run the uv tree
command. This will show you the dependency tree of your entire project, along with the installed versions.
uv tree
For development dependencies, UV keeps them separate. Just use the --dev
flag with the add
command. Here, we'll add the PyTest framework for development purposes only, not for the production build.
uv add pytest --dev
If we go back to the configuration file, you'll see that pytest
has been added under a [project.optional-dependencies]
group for development.
Keeping Your Environment in Sync
If someone else clones your project, they can run the uv sync
command to set everything up. This will read the lock file and recreate the exact same environment, ensuring consistent builds.
You can also use this command whenever you update the dependencies in your pyproject.toml
file, whether to add or remove a package. It will figure out what to do.
Let's try it. Go to your pyproject.toml
file and just remove sqlalchemy
from the dependencies. After saving the file, go back to the terminal and run:
uv sync
You'll see that it has uninstalled SQLAlchemy because it realizes the environment is now different from the configuration. If you run it again, nothing will happen because the environment and configuration are now in sync.
Alternatively, instead of editing the configuration file directly, you can remove a package through the command line:
uv remove redis
This will remove the package from your dependencies, your lock file, and your environment. Compared to pip
, this is a much cleaner approach to managing your packages.
Running Tools On-the-Fly with UVX
The last thing we're going to cover is uvx
, which is UV's solution for running Python tools without installing them globally or adding them to your project.
To demonstrate this, imagine you've intentionally messed up the formatting of your main.py
file. Let's say you want to format this code with black
, but you don't want to add it to your project dependencies. Without UV, you'd have to either install it globally, risking version conflicts, or use another tool like pipx
.
Once again, UV simplifies this. You can just run a tool using the uvx
command, followed by the name of the tool and any of its arguments.
uvx black main.py
Behind the scenes, UV automatically creates a temporary, isolated environment, installs the tool there, and then executes it. This is also cached, so it's much faster the second time around. After running the command, you'll see that the formatting in your editor is now fixed, and this action didn't affect your project dependencies at all.
Here are a few other examples of uvx
commands to give you an idea of how it works:
# Run a specific version of a tool
uvx --package cowsay==5.0 cowsay "Hello from uvx"
# Start a simple HTTP server
uvx http.server
# Run a Jupyter notebook
uvx jupyter lab
You can generally run any tool that is available on PyPI. If you have tools that you use frequently, you can also just install them globally but in an isolated way using uv tool install
.
Why This Matters for Modern Development
Recently, building AI agents that use MCP servers has become more common, and uvx
is especially relevant here because many MCP servers are developed in Python. If you're working with a CLI agent and want to use an MCP tool someone else has built, you can just use the uvx
command to spin up that tool without ever having to install it or set up an environment for it yourself. This one use case alone can be a powerful reason to start looking into UV.
A Note on Speed
Whenever UV is mentioned online, its speed is brought up as a significant advantage over traditional tools like pip
. This is partly because it is implemented in Rust, a high-performance language, and also because of its aggressive caching strategy.
However, the speed of a package manager has never really been a bottleneck for many developers. The focus of this article isn't on speed, because simplicity and reliability are often more important factors for choosing a tool. That said, one case where the speed of package and environment setup could be really useful is in a CI/CD pipeline, where the Python build step might be slow enough to be a bottleneck.
Try UV on Your Existing Projects
If you like the idea of UV, you don't have to wait until you start a new project. You can use it with your existing projects right now. Just install UV and then run uv init
in your project directory. Your existing pip
-based project will work fine, as UV can read the requirements.txt
file and help you migrate. Give it a go and see how it can streamline your workflow.
Join the 10xdev Community
Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.