Python GitHub Actions Workflow Template
Last updated
This is a working .github/workflows/ci.yml for a Python project. It caches pip downloads, tests on three Python versions, lints with ruff and runs pytest. Adjust the install step if you use Poetry or uv.
The workflow
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: pip
cache-dependency-path: |
requirements*.txt
pyproject.toml
- run: python -m pip install --upgrade pip
- run: pip install -r requirements.txt -r requirements-dev.txt
- run: pip install ruff pytest
- run: ruff check .
- run: pytest -q
What the non-obvious parts do
- Quoted versions.
"3.11"must be a string. Unquoted3.10is read by YAML as the number 3.1, a classic bug. cache: pipcaches pip's download cache, not the virtual environment. The key is built from the files named incache-dependency-path. If you leave it out,setup-pythonlooks forrequirements.txtonly. See caching pip.pip install ruff pytestis here so the template runs on a bare project. If these are already in your dev requirements, delete the line.permissions,concurrencyandtimeout-minutesare the same safety trio used in every template: read-only token, cancel superseded pull request runs, and a job time cap.pytest -qkeeps logs short. Add-xto stop at the first failure while debugging.
Variants
Poetry
Install Poetry with pipx install poetry, then use cache: poetry in setup-python and poetry install --no-interaction. Run commands with poetry run.
uv
Use astral-sh/setup-uv@v5 with enable-cache: true, then uv sync and uv run pytest.
Monorepo
Use working-directory: on steps, or defaults.run.working-directory on the job, and point cache-dependency-path at each package's requirements file. Add a paths: filter so only affected packages run.
Database tests
Add a services: block with Postgres; the Django template shows the full pattern.
Deploy job
Add a second job with needs: test and if: github.ref == 'refs/heads/main'. Build the wheel with python -m build and upload it as an artifact first.
macOS or Windows
Add os: [ubuntu-latest, windows-latest] to the matrix if you publish a library that must work on both.
Run it on faster machines
Test suites are often the slow part. Change one line:
runs-on: [self-hosted, Linux, compiler-ci]
The job then runs on a fresh 8 vCPU machine from compiler.dev, with automatic fallback to your original runner. Pair it with pytest-xdist (pytest -n auto) to use the extra cores, and see why pytest is slow and pricing.
FAQ
Should I cache the virtualenv?
Usually no. Caching pip's downloads is safer. A cached virtualenv can break when the Python patch version changes.
How do I test only one Python version on pull requests?
Use a matrix with all versions on main and a single version on pull requests, using include. See matrix builds.
Why does pip install take so long?
Packages with compiled extensions are built from source when no wheel exists for your Python version. Pin versions that ship wheels.
Can I speed up pytest?
Yes: pytest-xdist for cores, test sharding for machines.
Related
- .NET GitHub Actions Workflow TemplateCopy-paste GitHub Actions CI for .NET with NuGet caching, restore, build and test steps, a SQL Server service option, concurrency and permissions.
- Django GitHub Actions Workflow TemplateCopy-paste GitHub Actions CI for Django with a Postgres service container, pip caching, migrations check and tests, plus concurrency and permissions.
- Docker Build GitHub Actions Workflow TemplateCopy-paste GitHub Actions workflow to build Docker images with Buildx, GitHub cache layers and a push to GHCR on main, with minimal permissions.
- Elixir GitHub Actions Workflow TemplateCopy-paste GitHub Actions CI for Elixir and Phoenix with a Postgres service, deps and _build caching via erlef/setup-beam, plus format and tests.