UV python package and project manager
uv is a more modern tool to build environments and manage python projects. It’s built in Rust and claims to be extremely fast in resolving dependencies (10x-100x speedup compared to pip).
TLDR
General steps for creating a virtual environment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# install exec as
curl -LsSf https://astral.sh/uv/install.sh | sh -s -- --verbose
# install a python version and create virtual environment
uv python install 3.13
uv python pin 3.13
uv venv .venv
# install some packages
uv pip install numpy pandas
# see the list of packages and versions
uv pip list
# install from pyproject.com
uv venv .venv
uv sync
General steps to create a package (assuming we have python 3.13)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# create package structure
uv init --name=newpackage --build-backend=setuptools --package
# create first module (dumb file for example)
touch src/newpackage/modulea.py
echo "import numpy as np" >> src/newpackage/modulea.py
# add dependencies
uv add numpy pandas matplotlib
# see tree depencencies
uv tree
# lock/pin dependencies
uv lock
# build & publish
uv build && uv publish
Install uv
It is possible to install uv
system-wide using a bash installer for MacOS and Linux, I would recommend this if you decide uv
is your definite tool to build projects. If that’s the case, run on Linux or MacOS
1
curl -LsSf https://astral.sh/uv/install.sh | sh -s -- --verbose
that will install uv
in ~/.cargo/bin/uv
. Checking on the file install.sh
just the two binaries uv
and uvx
will be installed, not any other files. This is good, we just care about the binaries at this point.
Make sure you have ~/.cargo/bin/
directory in your PATH
variable and execute uv --help
to display the helper.
Managing Python versions
uv
is actually a great tool to substitute pyenv
if you don’t like the latter. With a simple command you can install and manage several python versions for your system:
1
uv python --help
Let’s install 3.13
version
1
uv python install 3.13
And see that it has been installed in a directory that you can get from uv python dir
, in my case (MacOS intel) ~/.local/share/uv/python
.
Running uv python list
will show all available versions (including the ones installed in pyenv
tool) and the ones you can install.
Managing Python environments
As mentioned before a python environment requires fist a python version, we can first fix the python version by running
1
uv python pin 3.13
And then we create the environment in .venv
as
1
uv venv .venv
Check that the python version for the new environment is the correct one with .venv/bin/python --version
.
Begin installing packages (no need to activate the environment if you are alrady in the directory where .venv
lives)
1
uv pip install numpy pandas
and check which packages are in the environment
1
uv pip list
Alternatively you can use uv pip freeze
.
Create a python project
uv
can manage the creation of a basic project structure. First select your python version
1
uv python pin 3.13
Then run
1
uv init --help
among many options you will see options for the kind of project to be built:
1
2
3
4
--package Set up the project to be built as a Python package
--no-package Do not set up the project to be built as a Python package
--app Create a project for an application
--lib Create a project for a library
Let’s create a package by running
1
uv init --name=newpackage --build-backend=setuptools --package
this will create a project with the structure (run tree
):
1
2
3
4
5
6
.
├── README.md
├── pyproject.toml
└── src
└── newpackage
└── __init__.py
if you cat pyproject.toml
you will get the definition of your project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[project]
name = "newpackage"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
newpackage = "newpackage:main"
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
Now you can start adding dependencies
1
uv add numpy pandas matplotlib ruff
This will automatically create a virtual environment (in .venv
) and install the package, also will modify the pyproject.toml
with the package.Note, you can remember dependencies running uv remove pkg
, for instance uv remove pandas
to remove pandas.
A very convenient tool is the tree
, we used pipdeptree
package previously but the nice thing about uv
is that this tooling comes by default. Just run
1
uv tree
that shows
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
newpackage v0.1.0
├── matplotlib v3.9.2
│ ├── contourpy v1.3.0
│ │ └── numpy v2.1.3
│ ├── cycler v0.12.1
│ ├── fonttools v4.54.1
│ ├── kiwisolver v1.4.7
│ ├── numpy v2.1.3
│ ├── packaging v24.1
│ ├── pillow v11.0.0
│ ├── pyparsing v3.2.0
│ └── python-dateutil v2.9.0.post0
│ └── six v1.16.0
├── numpy v2.1.3
└── pandas v2.2.3
├── numpy v2.1.3
├── python-dateutil v2.9.0.post0 (*)
├── pytz v2024.2
└── tzdata v2024.2
Finally you can even pin the dependencies using
1
uv lock
that will generate a file uv.lock
that contains the package version along with the hash and the specific wheel to download from pypi on every platform, similarly to what we have seen in pipvenv post in file Pipfile.lock
. As mentioned several times in these series of posts, one may want to use a locked environment when running a service and not when defining a package.
In some ocasions you may want to build and publish the python package, for that uv has the commands build
and publish
. We won’t get into the details of it in this post (we’ll have a later post dedicated to it), just remember that you can handle this with uv
.
A more complete pyproject.toml
and how to install in uv
and pip
In this section we’ll show a better pyproject.toml
and how to install it using pip
and uv
. One of the good things about uv
is that it seems to be completely compatible with pip
, the default dependency manager. That makes it ideal for any project as the default user won’t use uv
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
[project]
name = "newpackage"
version = "0.1.0"
description = "My new package"
authors = [
{ name = "the author" }
]
license = { text = "MIT license" }
readme = "README.md"
requires-python = ">=3.9,<3.13"
dependencies = [
"matplotlib>=3.9",
"numpy>=1.26,<2.0.0",
"pandas>=2.2",
"scipy>=1.13",
"tifffile>=2024.8",
"pip>=24.3.1",
]
[project.optional-dependencies]
dev = [
"coverage>=7.6",
"pytest>=8.3",
"ruff>=0.7",
]
# Ruff is a great tool for linting
[tool.ruff]
line-length = 99
[tool.ruff.lint]
select = [
# Pyflakes
"F",
# Pycodestyle & Warnings
"E",
"W",
# isort for unsorted imports
"I001"
]
[tool.ruff.format]
quote-style = "single"
indent-style = "space"
docstring-code-format = true
docstring-code-line-length = 20
# build system, use setuptools, the default for Python
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
# python CLI, structure scripts/my_script.py funcion main
# once installed the environment, activate it and run `my_cli` on terminal
# to run the CLI
[project.scripts]
my_cli = "scripts.my_script:main"
## In case you run a private pypi repository, uncomment and change URL
# [[tool.uv.index]]
# name="pypi-internal-server"
# url = "http://pypi-server.mydomain.com:8081/repository/"
# priority = "supplemental"
To install, create the enviornment and sync
1
2
3
4
5
uv venv .venv
uv sync
# to install with the optional dependencies (dev in our case)
uv sync --all-extras
Equivalently in pip you can do
1
2
3
4
5
6
python -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install . --extra-index-url http://pypi-server.mydomain.com:8081/repository/
# to install with optional dependencies
.venv/bin/python -m pip install .[dev] --extra-index-url http://pypi-server.mydomain.com:8081/repository/
Now, there’s a lot here, let me explain, the first part just specifies the python versions and the dependencies. Then we have the tool ruff
, more on that later, the build system (setuptools
as the default tool in python) and finally a CLI and an internal pypi repository.
Let me begin by ruff
tells you what lines of your code are not properly formatted (linting) and also formats them (changes the format of the code, a formater). Run it with uv run ruff check .
. Imagine you have a file with the following content in your package:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os
import sys # Unused import
def example_function(x, y):
return x + y
def unused_function(a, b): # Unused function
return a * b
print(example_function(1, 2))
def _make_ssl_transport(
rawsock, protocol, sslcontext, waiter=None,
*, server_side=False, server_hostname=None,
extra=None, server=None,
ssl_handshake_timeout=None,
call_connection_made=True):
'''Make an SSL transport.'''
if waiter is None:
pass
if extra is None:
extra = {}
Ruff (with the above configuration) here will complain in several places. In the first line it will raise an error because the imports are not sorted (using isort rule). In the first and second lines it will also raise error, this time F401
because the packages sys and os are imported but never used. The unused function will raise F841
and finally (not the case in this example) if any line would exceed 99 characters it would raise E501
. Ruff is good to keep you code clean, check all the rules running ` uv run ruff rule –all and all the linters with
uv run ruff linter. Once the errors are identified you can fix them as suggested by
ruff` with
1
uv run ruff check . --fix
Finally format the code with
1
uv run ruff format
In the documentation ruff developers say that “ the formatter is designed as a drop-in replacement for Black”. It indeed formats the function for us to a much nicer one!. Check the rules and the formater in the official documentation.
Finally we have added a private repository in the pyproject.toml
. The private repository is a URL where we can host wheels, the software artifacts containing a package (most of the times is basically a zipped repository). In some organizations we publish packages in an internal repository but by default python tries to find all packages in pypi. Adding the final lines with the appropiate URL will tell uv
that it may have to look at that repository too. We have defined this for uv
only in the pyproject.toml
, actually it is not possible to define it for pip
. In that case we simply add the extra index at the time of installation through --extra-index-url http://pypi-server.mydomain.com:8081/repository/
. An alternative for pip
is to add a pip.conf
file (see more details here).
This section ended up being a bit long but I wanted to show a good pyproject.toml
file with most of the things you need on a python package project. Hope it is helpful!. I will likely write two other posts about ruff
and how to setup your own pypi
server securely, in a much more detailed way.
Speed benchmark
uv promisses large speedups in resolving dependencies on their weppage, about 10x to 100x compared to pip. In this section we will test how fast each engine is capable of resonving basic dependencies. We will compare uv
with other popular tools like pip
, conda
, mabmba
and poetry
. I will use my 2019 macbook pro AMD with 16 GB of memory. Also to compare apples to apples I will start building from scratch each environment without caching packages, i.e. downloading all the packages each time. We will ask the dependency manager to install the following:
1
pandas scikit-learn flask fastapi matplotlib requests pytest boto3 pyyaml cryptography jupyterlab seaborn pillow sqlalchemy
on a python 3.11 version.
Let’s begin with the tool presented in this post, uv
.
uv
1
2
3
4
5
uv python install 3.11
uv python pin 3.11
rm -rf .venv
uv venv .venv
time uv pip install pandas scikit-learn flask fastapi matplotlib requests pytest boto3 pyyaml cryptography jupyterlab seaborn pillow sqlalchemy --no-cache-dir
with result:
1
3.08s user 8.33s system 148% cpu 7.668 total
pip
1
2
3
4
5
6
7
8
pyenv install 3.11.2
pyenv shell 3.11.2
rm -rf .venv
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
time pip install pandas scikit-learn flask fastapi matplotlib requests pytest boto3 pyyaml cryptography jupyterlab seaborn pillow sqlalchemy --no-cache-dir
with result:
1
32.32s user 7.25s system 66% cpu 59.662 total
Conda
Run
1
2
pyenv shell miniconda3-latest
conda remove --name myenv --all -y
create the file environment.yml
, this way we can time better the process
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
name: myenv
dependencies:
- python=3.11
- pandas
- scikit-learn
- flask
- fastapi
- matplotlib
- requests
- pytest
- boto3
- pyyaml
- cryptography
- jupyterlab
- seaborn
- pillow
- sqlalchemy
1
2
conda clean --all -y
time conda env create -f environment.yml
getting a time of 43 seconds.
Mamba
Run
1
2
pyenv shell mambaforge
conda remove --name myenv --all -y
create same file environment.yml
as the conda
section.
1
2
conda clean --all -y
time conda env create -f environment.yml
geting a time of 52 seconds.
Poetry
1
2
3
poetry config virtualenvs.in-project true
pyrenv shell 3.11.9
poetry env use 3.11.9
The pyproject.toml
that has to be placed in the project directory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[tool.poetry]
name = "speed-test"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "3.11.9"
pandas = "*"
scikit-learn = "*"
flask = "*"
fastapi = "*"
matplotlib = "*"
requests = "*"
pytest = "*"
boto3 = "*"
pyyaml = "*"
cryptography = "*"
jupyterlab = "*"
seaborn = "*"
pillow = "*"
sqlalchemy = "*"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Then run
1
time poetry install --no-cache
Which takes 25.05s user 14.10s system 100% cpu 38.877 total.
Benchmark conclusions
Summing up, uv
seems to be the fastest by far, around ~8x compared to pip
. Very promissing!
tool | time to resolve depencencies(seconds) |
---|---|
uv | 07.66 |
pip | 59.62 |
conda | 43 |
mamba | 52 |
poetry | 39.87 |
Conclusions
uv is not only super fast in resolving depencencies, it manages your python versions and by default creates the environment in the local directory. With uv
you don’t need anything else, no pyenv
for managing your python versions, or no poetry
to build and publish your wheels. Even creating a new project boilerplate is super easy!. I have been reading out there and seems that the only drawback is that the dependency management is a bit less strict compared to poety
. To me this is fine, this tool simply works. Another big plus of uv
is that it is perfectly compatible with pip
, this is, the pyproject.toml
defined for uv
can be installed using pip
seamlessly as it follows the standard of PEP-621 not forcing the users of your package to use uv
but the most general tool pip
if they want to.