← All guides

Python version matrix in GitHub Actions: test 3.10–3.14 without waste

A library that supports Python 3.10 through 3.14 has to test Python 3.10 through 3.14 — "works on my interpreter" is how you ship a syntax error to half your users. The version matrix is the tool, and the difference between a good one and a wasteful one is a handful of lines.

The basic matrix

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    strategy:
      matrix:
        python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'
      - run: pip install tox && tox

Two details people miss: quote the versions — unquoted YAML reads 3.10 as the float 3.1, and your "3.10" job silently tests 3.1 (it fails on resolve, if you're lucky); and set cache: 'pip' once — each matrix leg gets its own cache keyed on version + requirements (the pip caching guide has the details).

Don't multiply what you don't ship

A matrix is a multiplier: 5 Python versions × 3 operating systems is 15 jobs per push. Unless you genuinely ship platform-specific behavior, run the full version list on one OS and spot-check the others with include::

strategy:
  matrix:
    python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
    os: [ubuntu-latest]
    include:
      - { python-version: '3.14', os: macos-latest }
      - { python-version: '3.14', os: windows-latest }

scrapy is a real-world reference: its Ubuntu test workflow covers 3.10 through 3.14 plus PyPy via an include: list, with the extra dimensions (typing, docs, pinned-minimums) expressed as named legs instead of a full cartesian grid.

fail-fast, and when to turn it off

By default one red leg cancels the rest of the matrix (fail-fast: true). For a version matrix you usually want the opposite while debugging a version-specific failure — knowing it breaks on 3.14 only is the diagnosis:

strategy:
  fail-fast: false
  matrix:
    python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']

Keep the job timeout-minutes tight either way — a matrix multiplies a hung job's cost by its width (the timeout guide covers why). See how other popular Python repos run CI →

These hide across however many workflow files you have, which is exactly why nobody sits down and fixes them. Point GitSpider at your repo and it flags which patterns apply, with the fix for each.

Scan your repo free