Getting started with Python uv

posted on 2024-11-24

Almost 3 years ago, I wrote down a Getting started with Python Poetry. Now that I have started to use uv instead, I decided to run through the same example with uv instead of Poetry.

Why use uv

Whenever you write a program, there will come a time you start to require external modules to solve common problems. For example, use the requests library to make HTTPS requests.

You can easily install your requests manually, however if you want to later share your code with other people or run your code on other systems, you will have to install requests there manually again as well.

What if you have one program that requires requests version 1 and another which requires requests version 2? You can't have both version 1 and version 2 on the same PYTHONPATH, so you will have to start using virtual environments.

Another issue is making sure that all the libraries you want to use, actually work together. If you install requests 2.6 and then install urllib3 1.8 you will actually end up with a broken environment: requests wants urllib3 above version 1.21.1. If you simply pip install, the order of the pip install will actually matter. Solving this puzzle is best automated, and every modern language has a companion build tool to solve this dependency puzzle for you (Rust has cargo, Java has yarn, Haskell has stack, Javascript has npm, etc.).

For Python there are uv, poetry, pipenv, rye (now merged into uv), pip-tools, and pants. Let's dive into my current favorite: uv.

Installing uv

First install uv. If you have a package manager, use that. If not, my advice would be using pipx: pipx install uv. If you don't have pipx, install that first. For more information see the official documentation.

If it is installed correctly, you can run uv --version to get the version. During the writing of this blog, I used uv 0.4.24.

uv by example

Uv projects are managed using three configuration files: the pyproject.toml, uv.lock and .python-version. The pyproject.toml is a file you can edit manually, the uv.lock describes the exact versions found after solving the dependency puzzle and is fully managed by uv. Do not edit/touch this file, but do add it to your git repository. The .python-version is used to pin the version of Python you want to use during development. Although the pyproject.toml file will constraint your python version, it won't pin it.

In the next steps we create a tiny program from scratch using uv. The program will simply check if there is a holiday somewhere, and we will call it where is the party (witp).

Start a new project by running

uv init --package --project witp

This will create the above mentioned files, a sample Python file called hello.py and a README.md for documentation. All inside the witp folder.

Use any code editor you want inside the witp folder.

Open the src/witp/__init__.py and put the whole implementation in there:

import datetime
from typing import Optional, Tuple

import holidays


def main():
    country, holiday = where_is_the_party(datetime.date.today()) or (None, None)
    if country is not None:
        print(f"Go pack your bags, we need to go to {country} to celebrate {holiday}")
    else:
        print("Can't find the party, sorry")


def where_is_the_party(today: datetime.date) -> Optional[Tuple[str, str]]:
    for country in holidays.list_supported_countries():
        ch = holidays.CountryHoliday(country=country)
        if ch.get(today):
            return country, ch.get(today)
    return None

Inside the pyproject.toml, uv placed an entrypoint definition for us already:

[project.scripts]
witp = "witp:main"

Because of this snippet, we can run our program as a commandline tool. It is currently only available in our uv virtual environment, so we use uv to run it:

uv run witp

Oh oh.. we forgot about the holiday library and get

ModuleNotFoundError: No module named 'holidays'

Ok, so let's ask uv to add that dependency to the pyproject.toml:

uv add holidays

This will update the environment. So now if we run

uv run witp

we get:

Can't find the party, sorry

Hmmm... will it ever find a party? Let's add a test to make sure we are doing the right thing. Create the folder tests with test_witp.py and open it. Inside we put:

import datetime

from witp import where_is_the_party


def test_holiday():
    assert where_is_the_party(datetime.date(2020, 12, 25)) == (
        "AL",
        "Christmas Day",
    )

Now let's run those tests and see if it works:

uv run pytest

Oh no, another issue:

error: Failed to spawn: `pytest`
Caused by: No such file or directory (os error 2)

There is no pytest yet. At this point we don't want to run uv add pytest because that would make our witp package depend on pytest directly. Instead we want a special dependency we only use in development:

uv add --dev pytest

After this, let's try uv run pytest again:

uv run pytest

All green? Great!

Ok, so we have this great program, we need to share it with the world. Let's create a python package we can e-mail to a friend:

uv build

Now we have a package in the dist folder called witp-0.1.0-py3-none-any.whl. We can run pip install witp-0.1.0-py3-none-any.whl wherever we need it.

Summary

We create a project and package using the following commands:

  • uv init --package --project <new folder name> to get started with a new project.
  • uv run to execute a command in the virtual environment. We used pytest but we could just as well have executed python.
  • uv add to add a dependency to the project. Users of our package will have to install this as well to have our package work.
  • uv add --dev to add a dependency for development only. Developers will have to install this package to work on this project.
  • uv build to create a package.

There is way more you can do and learn when it comes to pyproject.toml configuration. This should get you up and running for your first project using uv based Python project.

Happy hacking!