Post

ENG | Discovering uv tool for Python packaging

Modern Python packaging on Linux with uv — why pip3 is blocked on openSUSE (PEP 668), and how uv replaces pip, pipx and pyenv in one tool.

ENG | Discovering uv tool for Python packaging

On openSUSE Slowroll/Tumbleweed installing or upgrading Python packages by pip3 no longer works, reason are documented in PEP 668 (Python Enhancement Proposal).

Also, I had some experience with Python scripts executed as systemd services or by systemd timers that broke on update, which is another motivation and after all, I tried openSUSE to see how it solved breaking changes on rolling distro.

As noted above, Python 3.13 packages or tools such as yt-dlp (Youtube downloader) were impossible to update so I eventually deleted whole .local/lib/python3.13 folder and .local/bin which seemed to contain only Python tools

Python PEP668

I was trying to ask various LLMs how to solve this problem, it seems that alternatives are

  • sudo zypper in python313-whatever except it’s system wide. Who knows how it survives a distribution upgrade.
  • Python virtual environment (python3 -m venv ~/zephyrproject/.venv, source ~/zephyrproject/.venv/bin/activate, pip install west), which are fixed to certain Python version.
  • Python environment (pyenv) - never tried.
  • pipx - never tried.
  • Modern tool is uv written in … wait for it … Rust. Never tried, let’s change it.

What is uv?

Package description says it

An extremely fast Python package and project manager, written in Rust.

Highlights:

  • A single tool to replace pip, pip-tools, pipx, poetry, pyenv, twine, virtualenv, and more.
  • 10-100x faster than pip.
  • Provides comprehensive project management, with a universal lockfile.
  • Runs scripts, with support for inline dependency metadata.
  • Installs and manages Python versions.
  • Runs and installs tools published as Python packages.
  • Includes a pip-compatible interface for a performance boost with a familiar CLI.
  • Supports Cargo-style workspaces for scalable projects.
  • Disk-space efficient, with a global cache for dependency deduplication.

AI generated image

Trying uv

Horrible name for search. On Fedora it’s installed by sudo dnf in python3-uv and it’s a binary. On openSUSE it’s installed using sudo zypper in python313-uv and there is a symlink to /usr/bin/alts which is some openSUSE specific tool for resolving multiple versions.

On Windows it can be installed using winget install astral-sh.uv

Useful commands are

CommandDescription
uvPrints help
uv pythonPrints help for python command
uv python listPrints available (and installed) Python versions
uv python install cpython-3.14.5-linux-x86_64Install Python 3.14.5
uv tool install mpremoteInstalls MicroPython terminal tool to ~/.local/bin
uv venv --python 3.14 ~/.venvs/default/Create default virtual environment
source ~/.venvs/default/bin/activateActivate virtual environment
uv pip install numpyInstalls numpy
deactivateLeave virtual environment

Some of my questions

1/ Are virtual environments actually the reason why to use #!/usr/bin/env python3 instead of #!/usr/bin/python3?

Absolutely, because it resolves to correct Python inside virtual environment.

2/ Where are Python binaries and how does tools locate Python?

Environments contain symlinks to cache. Installed tools have path to Python hardcoded.

This is explained by a screenshot of symlinks and scripts in .local/bin directory

Screenshot

3/ How to run scripts from systemd (or outside of venv)?

This surpringly works if Python is executed with path a pointing into virtual environment as shown below code.

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
#!/usr/bin/env python3
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm

def magic_kernel(x):
    x = np.abs(x)
    return np.where(x <= 0.5, 0.75 - x**2,
           np.where(x <  1.5, 0.5 * (x - 1.5)**2, 0.0))

x = np.linspace(-2, 2, 500)
y = magic_kernel(x)

plt.style.use("dark_background")
plt.rcParams["font.family"] = "Iosevka"

fig, ax = plt.subplots(figsize=(6.4, 4.8))
ax.plot(x, y)
ax.set_title("Magic Kernel")
ax.grid(True, alpha=0.3)

fig.patch.set_alpha(0.0)
ax.patch.set_alpha(0.0)

plt.savefig("/tmp/test.png", dpi=100, bbox_inches="tight", transparent=True)
print("saved to /tmp/test.png")
1
2
3
4
5
6
7
8
9
10
~/devel$ nvim test.py

~/devel$ ./test.py
Traceback (most recent call last):
  File "/home/pavel/devel/./test.py", line 2, in <module>
    import numpy as np
ModuleNotFoundError: No module named 'numpy'

~/devel$ ~/.venvs/default/bin/python3 test.py
saved to /tmp/test.png

Summary

Switching to uv solved two things at once. First, it made PEP 668 a non-issue — instead of fighting the system, there is now a clean workflow that keeps system Python untouched and personal environments completely separate. Second, and more importantly, scripts running from systemd timers or services are no longer fragile. Pointing ExecStart at the venv interpreter directly means no breakage after a system update. The script runs exactly the Python it was written for, every time.

The speed is a nice bonus — Python 3.14, numpy and seaborn installed in roughly five seconds total is genuinely surprising the first time. But that is just quality of life. The reliability and Python versions managed by user are the actual win.

And there is more to explore: project management.

Python, uv, seaborn All in one screenshot from nothing to seaborn plot

This post is licensed under CC BY 4.0 by the author.