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 form a producer → processor → aggregator data pipeline over a local NATS broker.

The pattern mirrors a real fleet where independent apps pin different Python versions from their own dependency sets and collaborate 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 runs on and the native dependencies it loaded:

journalctl -u app311 -o cat
{"event": "startup", "app": "app311", "role": "producer", "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": "processor", "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": "aggregator", "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. numpy diverges (2.4.6 on 3.11 vs 2.5.1 on 3.12 and 3.14) because each app resolved against its own interpreter.

The pipeline

The three apps form a data pipeline over NATS, each stage on its own interpreter:

  • app311 (producer, 3.11) generates a window of samples with numpy and publishes it on fleet.pipeline.raw.
  • app312 (processor, 3.12) consumes each window, reduces it to numpy statistics (mean, std, RMS, and the FFT peak frequency), and republishes on fleet.pipeline.processed.
  • app314 (aggregator, 3.14) consumes the statistics, fits a trend across a rolling window with scipy, and logs the result.

Watch the data flow through the first two stages:

journalctl -u app311 -o cat | grep produced # {"event": "produced", "seq": 7, "n": 64}
journalctl -u app312 -o cat | grep processed # {"event": "processed", "seq": 7, "peak_hz": 5.0}

Once the aggregator has seen a full window it logs pipeline_complete, whose chain names all three interpreters that touched the data:

journalctl -u app314 -o cat | grep pipeline_complete
{"event": "pipeline_complete", "seq": 11, "chain": {"producer": {"app": "app311", "python": "3.11.14"}, "processor": {"app": "app312", "python": "3.12.13"}, "aggregator": {"app": "app314", "python": "3.14.2"}}, "result": {"window": 3, "rms_mean": 0.763675, "rms_trend_slope": -0.007031, "peak_hz": 5.0}}

That single pipeline_complete line is the whole-system success signal: three apps, three interpreters, one device, collaborating on one result over NATS. The peak_hz: 5.0 is the tone app311 injected, recovered by app312's FFT — proof the data really flowed through every stage.

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