GitHub Actions CI/CD (5/10): Matrix Testing — Multiple Python Versions
Summary: Expand the CI workflow to test
hellociacross Python 3.10, 3.11, and 3.12 using a matrix strategy. One workflow file, three parallel test runs, proof that your package works everywhere you claim it does.
| Key | Value |
|---|---|
| Package name | helloci |
| Working directory | ~/projects/helloci |
| Workflow file | .github/workflows/ci.yml |
| Matrix versions | 3.10, 3.11, 3.12 |
| Runner | ubuntu-latest |
0. Prerequisites
- The
hellociproject with a working CI workflow from Part 4 pyproject.tomldeclaresrequires-python = ">=3.10"
1. Why Test Multiple Versions
Your pyproject.toml says requires-python = ">=3.10". That is a promise — anyone using Python 3.10, 3.11, or 3.12 should be able to install and use your package.
Without matrix testing, you only verify that promise on one Python version. A function that works on 3.12 might fail on 3.10 because of a syntax feature or standard library change that was introduced after 3.10. Matrix testing catches these problems before your users do.
2. Add a Matrix Strategy
Replace the contents of .github/workflows/ci.yml with this updated workflow.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
fail-fast: false
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install package and test dependencies
run: pip install -e ".[test]"
- name: Lint
run: ruff check .
- name: Check formatting
run: ruff format . --check
- name: Run tests
run: pytest -vCode language: YAML (yaml)
The only changes from Part 4 are the strategy block and the ${{ matrix.python-version }} variable in the setup step. Everything else stays the same.
3. Understand the Matrix
The strategy block:
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
fail-fast: falseCode language: YAML (yaml)
This tells GitHub Actions to run the check job three times — once for each value in the python-version list. Each run is an independent job on its own fresh VM.
| Key | What it does |
|---|---|
matrix.python-version | Defines the list of values to iterate over |
${{ matrix.python-version }} | Inserts the current value into a step |
fail-fast: false | All jobs run to completion even if one fails |
Note: The version numbers are quoted (
"3.10"not3.10) because YAML interprets3.10as the number3.1. This is a common gotcha — always quote version strings in YAML.
The fail-fast setting:
When fail-fast is true (the default), GitHub cancels all remaining matrix jobs the moment one fails. Setting it to false means all three Python versions run to completion. You see all failures at once instead of fixing them one at a time.
For a package with three supported versions, fail-fast: false is the right choice. You want to know the full picture.
4. Push and Watch the Matrix Run
Commit and push the updated workflow.
git add .github/workflows/ci.yml
git commit -m "Expand CI to matrix: Python 3.10, 3.11, 3.12"
git pushCode language: Shell Session (shell)
Open the Actions tab on GitHub. Click the workflow run. You now see three jobs instead of one.
check (3.10) ✓ check (3.11) ✓ check (3.12) ✓Each job has its own log. GitHub runs them in parallel — all three start at roughly the same time.
5. Read Matrix Results
Click any of the three jobs to see its log. The output is identical to a single-version run, except the Python version in the setup step matches the matrix value.
If one version fails and the others pass, you know the problem is version-specific. Common causes:
- Syntax features — f-string expressions added in 3.12,
matchstatement added in 3.10 - Standard library changes —
tomllibadded in 3.11,typingimprovements per version - Dependency compatibility — a dependency may drop support for older Python versions
Tip: When a matrix job fails, look at the Python version in the job name first. Then check whether the failing code uses features from a newer Python version.
6. Expanding the Matrix Later
You can add more dimensions to the matrix. For example, to test on multiple operating systems:
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
os: [ubuntu-latest, macos-latest, windows-latest]Code language: YAML (yaml)
This creates 9 jobs (3 versions x 3 operating systems). Each combination runs independently.
You do not need this for helloci right now. The point is that the matrix strategy scales — add a row and GitHub handles the rest.
Warning: More matrix combinations means more billable minutes. Free GitHub accounts get 2,000 minutes per month. A 9-job matrix uses minutes quickly if you push frequently.
Summary
You expanded the CI workflow to test helloci across Python 3.10, 3.11, and 3.12 using a matrix strategy. Three jobs run in parallel on every push and pull request.
strategy.matrixdefines the version list${{ matrix.python-version }}injects the current version into stepsfail-fast: falseensures all versions run even if one fails- YAML quoting — always quote version numbers to avoid
3.10becoming3.1
Your requires-python = ">=3.10" promise is now verified on every commit. In Part 6 you will add an integration test that connects to a real Postgres database using GitHub Actions service containers.
GitHub Actions CI/CD — All Parts
- 1 GitHub Actions CI/CD (1/10): Make It a Package You Can Test
- 2 GitHub Actions CI/CD (2/10): Unit Tests — Fast Feedback
- 3 GitHub Actions CI/CD (3/10): Quality Gate Before Tests — Lint and Formatting
- 4 GitHub Actions CI/CD (4/10): Your First CI Workflow — Run on Every PR
- 5 GitHub Actions CI/CD (5/10): Matrix Testing — Multiple Python Versions You are here
- 6 GitHub Actions CI/CD (6/10): Integration Tests — Real Dependencies in CI
- 7 GitHub Actions CI/CD (7/10): Branch Protection — Make CI a Merge Gate
- 8 GitHub Actions CI/CD (8/10): Artifacts — Keep Evidence From a Run
- 9 GitHub Actions CI/CD (9/10): Release Workflow — Tag, Build, Publish
- 10 GitHub Actions CI/CD (10/10): Going Professional — Split Jobs, Caching, and Nightly Builds
