Multiple Python Versions With Tox

I generally like to code in Python when doing my own projects not really sure why, I think it’s just different from my day job as a Java Developer. However when I do develop with Python I like to ensure that what do write works on multiple Python versions. The main reason being is that most of my stuff runs on single board computers and users might not have new versions of Python and I don’t like to restrict things. This is why I use pyenv to handle the installation of multiple versions of Python and then use tox to build all versions at once with tests and coverage. These tests and coverage results are also used when pushed to GitHub to ensure that everything is ok 😄

Setting up Pyenv

I feel it makes sense to first install pyenv and have that configured. We need to first set up the machine to be able to compile and install various Python versions, this is because pyenv goes to get the source for Python and builds it locally.

1
2
3
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

The next step is to update my bash profile so that pyenv is on the path and initialised. Adding the following to the end of the profile is enough.

1
2
3
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

Then the profile needs to be reloaded. Then check the versions.

1
2
3
4
source .bashrc
pyenv --version
--------
pyenv 2.4.14

Installing Python Versions

The next step is actually install the various Python versions, there’s a lot of choice which can be displayed with pyenv install –list. However I like to keep things simple and install the latest version of 3.9 -> 3.11 with the following.

1
pyenv install 3.8.20 3.9.20 3.10.12 3.11.10

This command will take a while depending on your machine, as it has to download and compile each version so go get a cup of tea. Once done it’s time to set what the default Python version is, in my case I set it to 3.10.12.

1
pyenv global 3.10.12

That should be all for pyenv, you can see what versions are installed at any time by running the following.

1
2
3
4
5
6
7
pyenv versions
----
3.8.20
  3.9.20
* 3.10.12 (set by /home/joseph/.pyenv/version)
  3.11.10
----

Setting up Tox

Before installing Tox I need to ensure that I have PIP installed globally, this is the tool that installs Python packages and will be used to install Tox globally as well. I recommend installing Pip and Tox globally because when running builds tox will create it’s own virtual environments for each of the Python versions. If tox was installed within a virtual environment it would only use the version of Python present in the virtual environment which is not what I want.

1
2
3
4
sudo apt update
sudo apt install -y
sudo -H apt install python3-pip # -H states install global
sudo -H pip install tox

Now that tox is installed I need to add yet another line at the end of my bash profile. Then reload it.

1
2
export VIRTUALENV_DISCOVERY=pyenv
source .bashrc

This should be all the configuration needed for getting tox with multiple versions of python set up. If you don’t have a project already set up for tox you can use this one. Clone this repository down, go into the root of the project and simply run tox, if you have the same versions of python configured as I have above it should run multiple builds one after the other with a code coverage report for each.

GitHub Action

The final step which isn’t really setting up multiple python versions with tox, but it’s somewhat related is to have the same thing happen on GitHub Actions. This allows tests and code coverage for each of the python versions I installed earlier to be triggered.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 and any other packages
        run: pip install tox
      - name: Run tox
        run: tox -e py
      - name: Coveralls
        uses: coverallsapp/github-action@v2

In the above I’m creating a matrix of python versions, similar to the versions that I created using pyenv. The first step is to check out the codebase. The second step is to setup Python on the runner, the version is set to the matrix defined earlier, this will make GitHub create runners for each of the versions of Python defined resulting in concurrent builds. An added option here is to cache the pip results, this results in faster builds later on, as the downloaded dependencies are saved locally to the runner and don’t have to be downloaded each time.

Once the runner has been set up, pip is used to install tox and run with the python version of the given environment. The final step is optional which is to take the code coverage report and publish it to coveralls which is great if you’re not aware of it.


↤ Maven and Java 21
Thinking About 2025 ↦