Testing Python Across Versions with pyenv and tox

Supporting more than one Python version is common when code runs on Raspberry Pis and other small boards that lag behind the latest releases. After a few breakages caused by assuming everyone had 3.10+, this setup became the default. It uses pyenv to install multiple Python versions side by side, tox to run the test suite against each version locally, and a small GitHub Actions workflow to do the same in CI.

Install pyenv and build prerequisites

pyenv compiles Python from source on your machine. Make sure the required build libraries are present, then install pyenv.

1
2
3
4
5
sudo apt update
sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
  libsqlite3-dev wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev \
  libffi-dev liblzma-dev
curl https://pyenv.run | bash

Add pyenv to your shell profile so new sessions pick it up:

1
2
3
4
# in ~/.bashrc (or ~/.zshrc)
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

Reload and verify:

1
2
3
source ~/.bashrc
pyenv --version
# pyenv 2.4.14

Install multiple Python versions

Pick the versions you need and install the latest patch releases for each. This example covers 3.8 through 3.11.

1
pyenv install 3.8.20 3.9.20 3.10.12 3.11.10

Choose a default for general work:

1
pyenv global 3.10.12

Confirm what is installed and which version is active:

1
2
3
4
5
pyenv versions
#   3.8.20
#   3.9.20
# * 3.10.12 (set by ~/.pyenv/version)
#   3.11.10

Install tox and point it at pyenv

Install tox so it can create per-version virtual environments and discover interpreters managed by pyenv.

1
2
sudo -H python3 -m pip install --upgrade pip
sudo -H python3 -m pip install tox

Tell tox to discover interpreters via pyenv:

1
2
3
# in ~/.bashrc
export VIRTUALENV_DISCOVERY=pyenv
source ~/.bashrc

Run a project across versions

Clone a project that is already wired for tox and run the full matrix locally. For example:

1
2
3
git clone https://github.com/joe-mccarthy/nsp-ntfy
cd nsp-ntfy
tox

Tox will spin up isolated environments for each configured Python, install your package and test dependencies into each one, and run the suite. Watching green across versions is a relief when you deploy to mixed environments.

Mirror the matrix in GitHub Actions

Use a simple matrix to test against the same versions in CI. This keeps pull requests honest and catches incompatibilities before they reach a device.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: ["3.8", "3.9", "3.10", "3.11"]
    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
          cache: "pip"

      - name: Install tox
        run: pip install tox

      - name: Run tox
        run: tox -e py

      - name: Coveralls
        uses: coverallsapp/github-action@v2

The matrix creates one job per Python version and runs them in parallel. The actions/setup-python step handles interpreter installation for each job. If you publish coverage, the Coveralls action can collect and report it. Coverage badges are optional, but they provide a quick signal.

Small habits that help

  • Pin minimum versions in pyproject.toml or setup.cfg. If you claim support for 3.8, codify it.
  • Keep the tox env list in one place. Many projects use envlist = py38, py39, py310, py311.
  • Test realistic dependency constraints. Old Pythons often require older libraries. Let the resolver do its job in each env.
  • Cache builds locally. Pyenv compiles can take a while the first time. The payoff is fast local switching later.
  • Run the matrix before tagging. A local tox run is still quicker feedback than waiting on CI.

This approach removed most of the version surprises. Local work stays on the preferred interpreter, but tests exercise the full support window. The same list flows into CI so pull requests cannot drift. The result is fewer breakages on devices that update more slowly and a smoother path to releases.

Thanks for reading. If you keep a lean tox config that handles optional extras or plugin matrices, share it so others can benefit.


↤ Previous Post
Next Post ↦