Skip to main content

Python Multi-Version with UV

View source on GitHub

This guide builds and runs three Python apps on one Avocado OS device, each pinned to a different CPython version (3.11, 3.12, 3.14), all installed with uv at build time. Two apps ship their own standalone interpreter inside their extension; one runs on the device's system Python. The three discover each other over a local NATS broker.

The pattern mirrors a real fleet where independent apps pin different Python versions from their own dependency sets and coordinate over a message bus.

Prerequisites

  • macOS 10.12+ or Linux (Ubuntu 22.04+, Fedora 39+)
  • Docker Desktop installed and running
  • The latest version of the Avocado CLI

For hardware targets, you will also need:

  • Your target device and any required accessories (SD card, USB cable, serial console adapter)
  • See the Support Matrix for your target's requirements

Initialize

Clone the reference or initialize a new project from it:

avocado init --reference python-multiversion-uv python-multiversion-uv
cd python-multiversion-uv

To target a Raspberry Pi 5 instead of the default QEMU target:

avocado init --reference python-multiversion-uv --target raspberrypi5 python-multiversion-uv
cd python-multiversion-uv

Install

Install the SDK toolchain, extension dependencies, and runtime packages:

avocado install -f

This pulls the SDK container image and installs nativesdk-uv, used by every app's compile step.

Build

Build the extensions and assemble the runtime image:

avocado build

Each app has its own compile step:

  • app311 and app314 run uv python install 3.11 / uv python install 3.14 inside the SDK. uv detects the target architecture automatically (native x86_64 for QEMU, emulated aarch64 for the Pi), downloads the matching python-build-standalone interpreter, installs each app's dependencies against it, and ships the interpreter plus app/packages inside the extension.
  • app312 installs its dependencies against the SDK's own Python (3.12, the same interpreter the device ships as /usr/bin/python3), so no interpreter is bundled.

Because each app resolves independently, versions diverge on purpose. On a recent build numpy resolves to 2.4.6 on 3.11 but 2.5.1 on 3.12 and 3.14 — exactly the isolation the reference demonstrates.

Deploy

QEMU

Launch the VM (serial console attaches to your terminal):

avocado provision dev
avocado sdk run -iE vm dev

SD card targets (Raspberry Pi, NXP, STMicroelectronics)

avocado provision -r dev --profile sd

Insert the SD card into the device and apply power.

Verify

Log in as root with an empty password. All three app services and the NATS broker start automatically on boot.

Each app logs a single startup line reporting the interpreter it is running on and the native dependencies it loaded:

journalctl -u app311 -o cat
{"event": "startup", "app": "app311", "role": "member", "python": "3.11.14", "executable": "/usr/lib/app311/python/bin/python3.11", "deps": {"numpy": "2.4.6", "nats-py": "2.15.0"}}
journalctl -u app312 -o cat
{"event": "startup", "app": "app312", "role": "member", "python": "3.12.13", "executable": "/usr/bin/python3", "deps": {"numpy": "2.5.1", "nats-py": "2.15.0", "mcap": "1.4.0"}}
journalctl -u app314 -o cat
{"event": "startup", "app": "app314", "role": "coordinator", "python": "3.14.2", "executable": "/usr/lib/app314/python/bin/python3.14", "deps": {"numpy": "2.5.1", "scipy": "1.18.0", "nats-py": "2.15.0"}}

The python field of each app is a different version, and every native dependency loads. app314 is the coordinator: it subscribes to the NATS fleet.hello subject, collects a hello from each interpreter, and logs fleet_complete once all three are present:

{"event": "roster", "seen": {"3.14": "app314", "3.11": "app311", "3.12": "app312"}}
{"event": "fleet_complete", "interpreters": ["3.11", "3.12", "3.14"]}

That single fleet_complete line is the whole-system success signal: three apps, three interpreters, one device, talking over NATS.

Customize

Change an app's dependencies

Edit the app's compile script (app311-compile.sh, app312-compile.sh, or app314-compile.sh) and add packages to the uv pip install line:

uv pip install --target "$APPDIR/packages" --python "$PYBIN" \
numpy \
nats-py \
pandas

Then add the import name and distribution name to the NATIVE list at the top of the app so the startup log reports it (app/overlay/usr/local/bin/app311.py):

NATIVE = [("numpy", "numpy"), ("nats", "nats-py"), ("pandas", "pandas")]

Change a pinned Python version

Edit the PYVER line in the compile script and the interpreter path in the systemd unit. For example, to move app311 from 3.11 to 3.13, set PYVER=3.13 in app311-compile.sh and update ExecStart in app311/overlay/usr/lib/systemd/system/app311.service to /usr/lib/app311/python/bin/python3.13.

Add a fourth app

Copy an app3NN extension block in avocado.yaml, its sdk.compile entry, and its three build scripts, then add it to the dev runtime's extensions list.

Rebuild after changes

avocado build
avocado provision dev