This is mostly an update to parts of the reproducible Python development box blog I've written up earlier. However, this blog post can be read as a stand-alone article.
At the time, I used a combination of poetry
to manage diffierent virtual environments for different projects, pyenv
to handle different python versions, and pipx
to manage python CLI independently of any project.
However, with the recent updates to uv (from the same makers of ruff, which I assume you already know about- but if not I highly recommend it as a replacement for flake8
, isort
and black
), the functionality of all of these tools has been combined into a single tool.
uv
has been touted as a fast replacement of pip
for a while, but recently its scope has greatly expanded (due to the incorporation of rye functionality, a tool which was adopted by Astral).
This offers a good opportunity to clean up the Python toolchain.
This will not be a detailed review of uv
functionality, because you can just read the documentation.
But I did want to update my previous blog now that uv
can be used to replace many (if not all) Python dependency management tools.
Dependency management
uv
replaces poetry
as a dependency management system.
There are multiple files in which dependencies can be specified, but using the uv init
command to create a skeleton project (very similar to poetry init
), will create a pyproject.toml
file.
This is where uv add
(similar to poetry add
) will add dependencies by default.
Since both tools use pyproject.toml
, one might assume migrating from poetry
to uv
will be easy,
but the configuration is different between tools, so be aware of this before migrating.
We will also have a uv.lock
file rather than a poetry.lock
, and the two are not compatible.
From the documentation you can probably figure out how to pilot this tool easily enough,
so I just wanted to highlight some differences with poetry
that I personally really like:
- By default,
uv
bundles the virtual environment with the repository folder under.venv
. Supposedlypoetry
has a configuration option for this as well, but I never found it. I prefer this because it makes runningpoetry shell
unnecessary.
Using uv run
(the analogue to poetry run
) will work and use this virutal environment, but I have the following in my config.fish
to make switching to virtual environments easier:
abbr -a venv source .venv/bin/activate.fish
-
The
--dev
flag exists foruv add
and is not deprecated. There are also optional dependencies that can be specified in yet another way (namely under the[project.optional-dependencies]
node). This is a bit redundant, but I like the convenience. -
Like in poetry, you can specify local dependencies, but they fall under their own configuration. An example looks like this:
[tool.uv.sources]
stockallocationmodel = { path = "../stockallocationmodel", editable = true }
network_planner = { path = "../networkplanning/network_planner", editable = true}
The uv
documentation itself recommends using workspaces for projects consisting of multiple Python packages,
but I personally haven't had the chance to try these out yet.
uv
does not have its own build backend, whilepoetry
does. Usinguv init
will by default give us the setup forhatchling
. Make sure you have these lines included, because otherwise I found builds will likely fail for arcane reasons:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Being written in Rust and therefore blazingly fast, it advertises an ability to fetch and install dependencies much faster than its competitor. Personally, anything that takes longer than 10 seconds falls under coffee break in my schedule, so I am not so susceptible to this argument.
Python version management
I personally love poetry
and wouldn't make the switch just for slightly faster install times.
However, poetry does not have a built-in way to manage different Python versions.
For this we can use pyenv
, but I didn't much care for it because of the redundant .python-version
file.
I often struggled having to manually synchronize the two and interact with pyenv
to get the versions right.
With uv
, Python version switching just works. Many commands have a --python
flag to specify versions,
and the uv python
command has a whole set of subcommands dedicated to managing Python versions.
There's built-in supportcpython
, obviously, but for pypy
as well.
Fuzzy search is provided, so uv python install pypy
gives me the latest pypy
version for my architecture.
In pyproject.toml
, there's a dedicated requires-python
setting which can let you specify minimum, maximum and exact python versions.
More complex version boundaries, such as '>=3.8,<3.10'
, are also possible.
But what's different is that the rest of the uv
commands will actually be proactive in providing that specific Python version,
as opposed to poetry
, which only prohibits you from proceeding until you figure out yourself how to get the right version yourself.
This functionality the main reason for me to make the switch, because it's extremely convenient to not have to struggle with Python versions anymore.
The fact that it consolidates two tools into one, means it makes pyenv
redundant.
You can also use uv python pin
to create a .python-version
file, and use
uv
as a pyenv
replacement for poetry
projects (if you really wanted to for some reason).
Python CLI tools
For tools in Python that you don't want to tie to a specific virtual environment, the uv tool
set of commands exists.
This is a pipx
replacement, essentially, and uv
has the alias uvx
just to drive this point home.
The main commands work as expected. That is, similar to pipx
, which in turn work similar to pip
(uv also has uv pip
, which is its pip
replacement).
list
, install
and upgrade
and uninstall
are all present, as expected.
Unfortunately, pipx inject
is not (this is a command that lets you add additional packages into the virtual environments pipx
erects around a tool, which can be useful if you want more control over the dependencies of a Python tool).
For unix systems, uv
will put symbolic links in your .local/bin
directory that direct to the tools it installs, so that these tools are available directly from the shell.
However, you can also use uvx {toolname}
if you prefer to call it this way, or in case of path conflicts.
A point of interest to note is that uvx {toolname}
will automatically fetch tools that it cannot find locally.
E.g. uvx ruff
would fetch ruff
from its own cache or download it, and run it as a one-off command.
This will not add it to your list of tools (uv tool list
).
Personally I'm not a fan of commands doing multiple things in this way, but it is clever how uv
leverages its own cache to quickly provide tools on-the-fly.
Conclusion
This was meant to be a recommendation and update blog rather than a full feature review.
Not mentioned here is that uv
replaces many other Python tools, such as pip
or the virtualenv
package,
which in turn already have replacements that fully or partially overlap with the functionality of the tools I mentioned in this blog.
Taking this into account, it could be argued that uv
is yet another incarnation of the competing standards problem that causes tools to propagate infinitely.
However, I've been sold on uv
pretty much the day I started using it.
I admit I was an easy sell, because the poetry
/pyenv
interop had been annoying me for a while, but I am still very impressed with the ease-of-use of uv
once I figured out the commands essential to my workflow.
Furthermore, astral has already shown it is capable of providing useful but not bloated replacement tools with ruff
,
which is also a must-have in all Python projects for me and many others.
Considering this, and the relative feature completeness of uv
, it seems like a sure bet to learn and use for your Python projects.