Japanese Woodblock Art - Utagawa Hiroshige’s ‘Seba’

Astral.sh recently released a new tool called uv and it replaces pip, pyenv, poetry and more!

This is something I saw on Hacker News a little while back and meant to try but never really had the opportunity. Now that I’m planning on writing another blog post which requires starting a new Python project I figured this was the perfect opportunity.

They give you a few installation options, I chose the classic curl option.

curl -LsSf https://astral.sh/uv/install.sh | sh

It installs in a few seconds then asks you to restart your shell. I do that and I’m ready to go.

They claim in their documentation that uv is 80 times faster than python -m venv at creating a virtual environment, so that’s something I’d like to test. Not because I need my venv creation to be quick but I just want to see how accurate their marketing material is to real life.

speed comparison from their website

uv

$ time uv venv venv_a
Using Python 3.10.7 interpreter at: /Users/john/.pyenv/versions/3.10.7/bin/python3
Creating virtualenv at: venv_a
Activate with: source venv_a/bin/activate

real	0m0.250s
user	0m0.043s
sys	0m0.038s

venv

$ time python -m venv venv_b

real	0m2.012s
user	0m1.537s
sys	0m0.251s

Interesting. I ran these tests a dozen times and the results weren’t any different. Since we really only care about the real time, it looks like uv creates a virtual environment in 0.25s and venv does it in 2s.

While this is faster, roughly 8x, its not the 80x they reported, but I’m going to say that since their marketing material was only off by an order of magnitude, it counts as “accurate” (lol).

The extra output is nice though, I really like the line showing me which interpreter I’m using: Using Python 3.10.7 interpreter at: /Users/john/.pyenv/versions/3.10.7/bin/python3

And the line Activate with: source venv_a/bin/activate will be helpful for beginners.

There is another cool feature of uv, where if you create a venv like so: uv venv and it doesn’t exist, it creates it, but if you run that same command again it will detect the venv exists and automatically source it for you which is neat.

Now lets test some package installs. Since I’m starting work on another blog post I’ll just use that for my tests.

I created a simple requirements.txt file with the following:

ollama
fastapi

Then ran (venv) $ time pip install -r requirements.txt

This took 2.7s of wall clock time and grabbed [email protected] and [email protected]

Lets see what happens when we do the same but using uv as a drop-in replacement.

I call uv venv to create the virtual environment and then uv venv again to source it and this is where I ran into the first problem. The docs say “When using the default virtual environment name, uv will automatically find and use the virtual environment during subsequent invocations.” but when I do that it isn’t automatically sourcing the bin/activate file. Weird.

After manually sourcing the file I ran the same install command but with uv instead of pip this time:

(venv) $ time uv pip install -r requirements.txt

This took 0.6s and gave me a pretty cool summary afterwards of all the packages it installed and what versions they were.

It also resolved the ollama and fastapi packages to the same version as pip which is mainly what I was testing.


The next feature I want to try is project initialization and management

$ uv init --name fastapi-ollama --lib
Initialized project `fastapi-ollama`

It creates a nice pyproject.toml for us that looks like this:

[project]
name = "fastapi-ollama"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = []

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

Let me just say here, I’m extremely happy to see that uv is generating PEP-621 compliant pyproject files, unlike Poetry. The fact that it’s creating and using project.dependencies instead of Poetry’s tool.poetry.dependencies is a major reason for me to make the switch.

It also created a new file src/fastapi-ollama/__init__.py which contains a simple hello() function.

Interestingly it defaults to using hatch as the build backend. It also created a src directory, which means they prefer a src-layout as opposed to the flat-layout which I’m used to seeing.

Now lets build and run it. According to the docs we add an entrypoint to the toml file:

[project.scripts]
hello = "fastapi_ollama:hello"

NOTE: The actual package and directory name is fastapi-ollama (with a hyphen) but python modules require you use an underscore, hence the line above being fastapi_ollama and not fastapi-ollama.

Then we build and run with uv run hello and we get:

Built fastapi-ollama @ file:///Users/john/prog/python/fastapi-ollama
Uninstalled 1 package in 0.90ms
Installed 1 package in 1ms
Hello world!

Now lets add a dependency and run it

uv add requests

That added the following to the pyproject.toml:

dependencies = [
    "requests>=2.32.3",
]

Ok lets update the hello() function to use requests, i’ll just grab the weather, and then we can test it again.

import requests

def hello() -> str:
    url = 'https://wttr.in?format=3'
    return requests.get(url).text
$ uv run hello
Raleigh, North Carolina, United States: ⛅️  +66°F

Cool, works very similarly to Poetry which is what i’ve been using the past few years.


Another major feature of interest to me is the ability for uv to manage your installed python versions.

$ uv python list
cpython-3.12.3-macos-aarch64-none     /opt/homebrew/opt/python@3.12/bin/python3.12 -> ../Frameworks/Python.framework/Versions/3.12/bin/python3.12
cpython-3.11.7-macos-aarch64-none     /opt/homebrew/opt/python@3.11/bin/python3.11 -> ../Frameworks/Python.framework/Versions/3.11/bin/python3.11
cpython-3.10.7-macos-aarch64-none     /Users/john/.pyenv/versions/3.10.7/bin/python3.10
cpython-3.10.7-macos-aarch64-none     /Users/john/.pyenv/versions/3.10.7/bin/python3 -> python3.10
cpython-3.10.7-macos-aarch64-none     /Users/john/.pyenv/versions/3.10.7/bin/python -> python3.10
pypy-3.10.14-macos-aarch64-none       <download available>
pypy-3.9.19-macos-aarch64-none        <download available>
pypy-3.8.16-macos-aarch64-none        <download available>

As you can see I usually use pyenv to manage my interpreters, so the ability for uv to do it as well would be great.

I really like that fact that it shows me all my Python installs and not just the ones that were installed via uv.

A quick test to install a version I don’t have:

$ uv python install 3.12.4
Searching for Python versions matching: Python 3.12.4
Installed Python 3.12.4 in 1.02s
 + cpython-3.12.4-macos-aarch64-none
$ uv python list
...
cpython-3.12.4-macos-aarch64-none     /Users/john/.local/share/uv/python/cpython-3.12.4-macos-aarch64-none/bin/python3

Well thats cool, when I first heard about uv I didn’t think it’d be this easy to use OR that it could replace both Poetry and pyenv.


The last feature I want to test is installing Python projects with uv inside Docker containers.

If it’s easier than Poetry then I’ll proceed to start using uv in all my new projects and eventually migrate old ones if I decide to stick with it.

According to their docs there are 3 ways to use uv in a Dockerfile:

  1. Use one of their pre-built images

    FROM uv:python3.12-alpine

  2. Copy the binary from their distroless image into yours

    COPY --from=ghcr.io/astral-sh/uv:0.4.8 /uv /bin/uv

  3. Install it via the regular installer (not recommended)

    We’re going to skip this step since it isn’t recommended

To keep things simple i’ll just use one of their pre-built images for now. Here’s what my test Dockerfile looks like:

FROM ghcr.io/astral-sh/uv:0.4.9-python3.10-alpine

WORKDIR /app

COPY . /app

RUN uv sync --frozen

CMD ["uv", "run", "hello"]

Now we build and run the image

$ docker build -t uv-test:dev .

$ docker run uv-test:dev

Raleigh, North Carolina, United States: ⛅️  +65°F

Exactly as expected!


My first impression of uv is very good. It replaces many tools I already use, its fast, and even though it’s new (their first release was earlier this year) and isn’t 1.0 yet, they release often and the developers seem to engage with the community well.

As you probably noticed from the package name I chose in my examples I’m going working on integrating FastAPI with Ollama. For the past few months Ollama is something I’ve spent a lot of free time playing around with and I’ve been enjoying it a lot and wanted to write some basic blog posts about it.