Getting started with Python uv
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 usedpytest
but we could just as well have executedpython
.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!