How I Test My Python Code Across Multiple Versions

Look, I’m a Java developer by day, but for some reason, I always gravitate toward Python for my personal projects. Maybe it’s just nice to have a change of pace from my day job? Whatever the reason, I’ve found myself needing to support multiple Python versions because a lot of my code runs on Raspberry Pis and other single-board computers where users might be stuck on older Python versions.

After breaking my code one too many times by assuming everyone had Python 3.10+, I finally got serious about testing across versions. That’s where pyenv and tox come in - they’re absolute lifesavers for this stuff.

Setting up Pyenv

First things first - you need to install pyenv. It basically lets you install multiple Python versions side by side without them fighting each other. But before you do that, you need to make sure your system can actually build Python from source (yes, pyenv compiles Python locally, which caught me off guard the first time).

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

I spent an embarrassing amount of time trying to figure out why pyenv wasn’t working before realizing I needed those dependencies. Don’t be like me - just install them all up front.

Next, you need to update your bash profile. I just add these lines to the end of my .bashrc:

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

Then reload your profile and check if it’s working:

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

Installing Python Versions

The next step is actually installing the various Python versions. There’s a ton of choices (check them with pyenv install --list), but I like to keep things simple and just grab the latest patch versions of Python 3.8 through 3.11:

1
pyenv install 3.8.20 3.9.20 3.10.12 3.11.10

Fair warning: this takes FOREVER. It’s downloading and compiling four separate Python versions from scratch, so go grab a coffee or three.

Once that’s (finally) done, set your default Python version:

1
pyenv global 3.10.12

I usually go with 3.10 as my default since it’s a good middle ground - stable but with most of the newer features I care about. But pick whatever makes sense for your main development work.

You can check that everything’s working by running:

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
----

The asterisk shows which version is currently active. Pretty neat, right?

Setting up Tox

Now for tox. I spent a whole evening banging my head against the wall when I first tried to get this working, so I’ll save you the trouble.

First, I recommend installing tox globally rather than in a virtual environment. That’s because tox creates its own virtual environments for each Python version anyway, so if you install tox inside a venv, it gets confused about which Python versions to use.

1
2
3
4
sudo apt update
sudo apt install -y
sudo -H apt install python3-pip # -H flag means install globally
sudo -H pip install tox

Next, add one more line to your .bashrc (I know, I know, we’re really cluttering it up at this point):

1
2
export VIRTUALENV_DISCOVERY=pyenv
source .bashrc

This tells tox to look for Python versions managed by pyenv instead of trying to find them on its own.

That should be it! To test if everything’s working properly, grab one of my projects that’s already set up for tox:

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

If all is well, you should see tox spinning up separate environments for each Python version and running your tests in each one. It’s pretty satisfying to watch all those tests pass across different versions!

GitHub Action

The last piece of the puzzle is making GitHub do all this testing automatically. Here’s a simple GitHub Action that will test your code against multiple Python versions:

 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

The coolest bit here is the matrix strategy - GitHub will automatically create separate jobs for each Python version and run them in parallel. Way faster than running them sequentially like we do locally!

I also push my coverage reports to Coveralls, which gives you those nice badges you can put in your README. It’s totally optional, but it’s pretty satisfying to see that coverage percentage tick up as you write more tests.

And that’s it! Now you can develop Python with confidence, knowing your code works across multiple versions. Trust me, this setup has saved me countless hours of debugging when deploying to different environments. Worth every minute spent setting it up!


↤ Previous Post
Next Post ↦