
uv is a Python package and project management tool.1 Maturin is a build backend for Python extension modules implemented in Rust. How to best use them together?
Why uv?
First of all, if you’ve already got Maturin, why do you need uv? There are a couple of reasons:
- uv takes care of the virtualenv management.
- You can use uv to install development dependencies such as pytest.
You could use another virtualenv manager such as tox. Myself, I wanted to use uv because I’m already familiar with it and it’s fast.
Setting up the project
Start by creating a new project the usual way:
maturin new my_project
Among other things, this creates a standard pyproject.toml
that works with uv.
If you run now uv sync
, uv will build the project using Maturin’s build backend, create a virtualenv and install the package there.
However, there is a problem: when you edit your Rust source files, there’s no easy way to rebuild the package.
Running uv sync
will not help because uv does not know that the Rust source files affect the package.
Running maturin develop
does rebuild the package, but if you do anything with uv that triggers a sync - for example, using uv run
- then it will re-install the cached old version of the package.
Below, I present three options for making it work.
Option 1: Let uv handle the rebuild
You can tell uv that the package needs to rebuild whenever the Rust source changes.
To do that, you’ll need to set tool.uv.cache-keys
in pyproject.toml
:
[tool.uv]
cache-keys = [{file = "pyproject.toml"}, {file = "Cargo.toml"}, {file = "**/*.rs"}]
Note that even if you have a mixed Python/Rust package, the package does not need to rebuild when the Python files change.
uv sync
by default installs the package as editable, so the Python interpreter uses the source files directly.
There’s a downside to this approach: uv sync
builds the package in the release mode which yields faster code but slower build times
than the debug mode used by maturin develop
.
Option 2: Use maturin develop
The alternative is to not install the project package at all when running uv sync
.
Edit pyproject.toml
:
[tool.uv]
package = false
Now if you run uv sync
, it will install dependencies but not the project itself.
You need to install it manually with Maturin:
maturin develop
You will have to re-run this command whenever you change the Rust files.
Option 3: Use maturin import hook
If you want to use maturin develop
, but you also want the code to rebuild automatically whenever it changes,
then the Maturin import hook is the way to go.
It rebuilds the code if needed when you import
it in Python.
Like in option 2, you need to set tool.uv.package
to false.
You also need to set the minimum required Python version to 3.9 or higher - the default by maturin new
is 3.8:
[project]
# ...
requires-python = ">=3.9"
Then, let’s install the import hook:
uv add --dev maturin_import_hook
uv run -m maturin_import_hook site install
maturin develop
You need to install the project with maturin develop
once, but after that the import hook will take care of the rebuilds.
Gotcha: If you’re on macOS and you use Python installed with Mac Homebrew, this will not work with Maturin 0.3.0.
A workaround is to use uv-managed Python: uv sync --managed-python
Managing Python dependencies
You can manage Python dependencies with the usual uv
commands.
For example, to add pytest as a development dependency:
uv add --dev pytest
-
In my opinion, uv is the snappiest and the easiest-to-use package management tool for Python right now and it’s the one that I recommend to most people. I’ve previously recommended Poetry, but uv has surpassed it in features and it was always much faster. ↩︎