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.
| |
Add pyenv to your shell profile so new sessions pick it up:
| |
Reload and verify:
| |
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.
| |
Choose a default for general work:
| |
Confirm what is installed and which version is active:
| |
Install tox and point it at pyenv
Install tox so it can create per-version virtual environments and discover interpreters managed by pyenv.
| |
Tell tox to discover interpreters via pyenv:
| |
Run a project across versions
Clone a project that is already wired for tox and run the full matrix locally. For example:
| |
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.
| |
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.tomlorsetup.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.