---
# === IDENTITY ===
id: software/devops/github-actions-python/2026
canonical_question: "GitHub Actions reference: Python CI/CD pipeline"
aliases:
  - "GitHub Actions Python workflow"
  - "Python CI pipeline GitHub Actions pytest"
  - "GitHub Actions Python tox matrix testing"
  - "Python GitHub Actions lint test deploy"
entity_type: software_reference
domain: software > devops > GitHub Actions Python CI/CD
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-28
confidence: 0.93
version: 1.0
first_published: 2026-02-28

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "actions/setup-python@v5 replaced v4 (Oct 2023); Python 3.7 EOL (Jun 2023)"
  next_review: 2026-08-27
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Always pin Python version explicitly (e.g., '3.12') -- never use just 'python3' which varies across runner images"
  - "Use pip install -r requirements.txt with pinned versions -- avoid floating version specifiers in CI"
  - "Python 3.7 is EOL -- minimum version for new projects should be 3.9+"
  - "Pin action versions to major tags (e.g., actions/setup-python@v5) -- never use @main in production"
  - "Never store PyPI tokens or API keys in workflow files -- use GitHub Secrets and OIDC trusted publishing"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need Node.js CI/CD pipeline instead of Python"
    use_instead: "software/devops/github-actions-nodejs/2026"
  - condition: "Need Docker image build and push workflow"
    use_instead: "software/devops/github-actions-docker/2026"
  - condition: "Need GitLab CI instead of GitHub Actions"
    use_instead: "software/devops/gitlab-ci-pipeline/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "test_framework"
    question: "Which test framework does your Python project use?"
    type: choice
    options: ["pytest", "unittest", "tox", "nox"]
  - key: "package_type"
    question: "What type of Python project is this?"
    type: choice
    options: ["application (Django/Flask/FastAPI)", "library (published to PyPI)", "script/CLI tool", "data science/ML"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/github-actions-python/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-28)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/devops/github-actions-nodejs/2026"
      label: "GitHub Actions Node.js CI/CD Pipeline"
    - id: "software/devops/github-actions-docker/2026"
      label: "GitHub Actions Docker Build and Push"
  solves: []
  alternative_to:
    - id: "software/devops/gitlab-ci-pipeline/2026"
      label: "GitLab CI Basic Pipeline"
  often_confused_with:
    - id: "software/devops/github-actions-nodejs/2026"
      label: "GitHub Actions Node.js CI/CD Pipeline"

# === SOURCES ===
sources:
  - id: src1
    title: "Building and testing Python"
    author: GitHub
    url: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src2
    title: "actions/setup-python: Set up your GitHub Actions workflow with a specific version of Python"
    author: GitHub
    url: https://github.com/actions/setup-python
    type: official_docs
    published: 2024-06-01
    reliability: authoritative
  - id: src3
    title: "Workflow syntax for GitHub Actions"
    author: GitHub
    url: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src4
    title: "Python in GitHub Actions"
    author: Hynek Schlawack
    url: https://hynek.me/articles/python-github-actions/
    type: technical_blog
    published: 2024-03-15
    reliability: high
  - id: src5
    title: "Publishing package distribution releases using GitHub Actions CI/CD workflows"
    author: PyPA
    url: https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
    type: official_docs
    published: 2024-01-01
    reliability: authoritative
  - id: src6
    title: "Security hardening for GitHub Actions"
    author: GitHub
    url: https://docs.github.com/en/actions/security-for-github-actions/security-hardening-for-github-actions
    type: official_docs
    published: 2024-01-15
    reliability: authoritative
  - id: src7
    title: "Automated Python Unit Testing Made Easy with Pytest and GitHub Actions"
    author: Pytest with Eric
    url: https://pytest-with-eric.com/integrations/pytest-github-actions/
    type: technical_blog
    published: 2024-05-10
    reliability: moderate_high
---

# GitHub Actions: Python CI/CD Pipeline

## TL;DR

- **Bottom line**: GitHub Actions provides native CI/CD for Python projects with matrix testing across Python versions, built-in pip/poetry caching, pytest integration, and PyPI publishing via trusted publishing (OIDC) -- all in a single YAML workflow.
- **Key tool/command**: `actions/setup-python@v5` with `cache: 'pip'` and `pip install -r requirements.txt` for reproducible environments.
- **Watch out for**: Not pinning dependency versions in `requirements.txt` -- floating versions cause non-deterministic builds and hard-to-debug CI failures.
- **Works with**: Python 3.9, 3.10, 3.11, 3.12, 3.13 on ubuntu-latest, windows-latest, macos-latest runners.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Always pin Python version explicitly (e.g., `'3.12'`) -- never use just `python3` which varies across runner images.
- Use `pip install -r requirements.txt` with pinned versions -- avoid floating version specifiers like `>=` in CI.
- Python 3.7 and 3.8 are EOL -- minimum version for new projects should be 3.9+.
- Pin action versions to major tags (e.g., `actions/setup-python@v5`) -- never use `@main` in production workflows.
- Use OIDC trusted publishing for PyPI uploads -- never store PyPI passwords as secrets (use `id-token: write` permission).

## Quick Reference

| Setting | Value / Command | Notes |
|---|---|---|
| Workflow file location | `.github/workflows/*.yml` | Any YAML file in this directory |
| Trigger on push | `on: push: branches: [main]` | Also supports pull_request, schedule, workflow_dispatch |
| Checkout code | `actions/checkout@v4` | Always first step in a job |
| Setup Python | `actions/setup-python@v5` | Supports python-version, cache, architecture |
| Python version from file | `python-version-file: '.python-version'` | Also supports pyproject.toml |
| Cache pip dependencies | `cache: 'pip'` on setup-python | Requires requirements.txt or pyproject.toml |
| Install dependencies | `pip install -r requirements.txt` | Or `pip install -e ".[dev]"` for editable install |
| Run tests (pytest) | `pytest --tb=short -q` | Add `--junitxml=report.xml` for CI reports |
| Run tests (tox) | `tox -e py` | Uses Python version from PATH |
| Run linter | `ruff check .` or `flake8` | Ruff is 10-100x faster than flake8 |
| Type checking | `mypy --strict src/` | Optional but recommended |
| Format check | `ruff format --check .` or `black --check .` | Non-destructive in CI |
| Matrix strategy | `strategy: matrix: python-version: ['3.10', '3.11', '3.12', '3.13']` | Runs in parallel |
| Upload artifacts | `actions/upload-artifact@v4` | Test reports, coverage, wheels |
| Publish to PyPI | `pypa/gh-action-pypi-publish@release/v1` | Trusted publishing via OIDC |

## Decision Tree

```
START
├── Library published to PyPI?
│   ├── YES → Matrix [3.10, 3.11, 3.12, 3.13] + tox + build + publish via OIDC
│   └── NO ↓
├── Django/Flask/FastAPI application?
│   ├── YES → Single Python version + pytest + deploy job with environment protection
│   └── NO ↓
├── Data science / ML project?
│   ├── YES → Single version + conda/mamba setup + large runner for GPU tests
│   └── NO ↓
├── CLI tool?
│   ├── YES → Matrix versions + pytest + build with PyInstaller or shiv
│   └── NO ↓
└── DEFAULT → Basic CI: checkout → setup-python → pip install → pytest
```

## Step-by-Step Guide

### 1. Create the workflow file

Create `.github/workflows/ci.yml` in your repository. GitHub automatically detects and runs workflows from this directory. [src1]

```yaml
name: Python CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.10', '3.11', '3.12', '3.13']
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'
      - run: pip install -r requirements.txt
      - run: pytest
```

**Verify**: Push to GitHub → Actions tab → see workflow run with 4 parallel matrix jobs.

### 2. Add linting and type checking

Run linters and type checkers before tests to fail fast on code quality issues. [src4]

```yaml
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'
      - run: pip install ruff mypy
      - run: ruff check .
      - run: ruff format --check .
      - run: mypy --strict src/
```

**Verify**: Introduce a lint error → push → workflow fails at the ruff check step.

### 3. Configure pip caching

The `cache: 'pip'` option on setup-python caches the pip download directory. It requires a dependency file to hash for the cache key. [src2]

```yaml
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'
          cache-dependency-path: |
            requirements.txt
            requirements-dev.txt
```

**Verify**: Check workflow logs for "Cache restored" message on the second run.

### 4. Add test coverage with pytest

Generate coverage reports and upload them as artifacts. [src7]

```yaml
      - run: pip install pytest pytest-cov
      - run: pytest --cov=src --cov-report=xml --cov-report=html --junitxml=report.xml
      - name: Upload coverage
        if: matrix.python-version == '3.12'
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: htmlcov/
          retention-days: 14
```

**Verify**: After workflow completes → Artifacts section shows coverage-report download.

### 5. Add tox for multi-environment testing

Use tox when your library supports multiple Python versions with different dependency sets. [src4]

```yaml
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.10', '3.11', '3.12', '3.13']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pip install tox tox-gh-actions
      - run: tox
```

**Verify**: `tox -l` locally shows all configured environments → GitHub Actions runs matching envs.

### 6. Publish to PyPI with trusted publishing

Use OIDC trusted publishing -- no passwords or tokens needed. Configure the trusted publisher on PyPI first. [src5]

```yaml
  publish:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'release'
    environment: pypi
    permissions:
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install build
      - run: python -m build
      - uses: pypa/gh-action-pypi-publish@release/v1
```

**Verify**: Create a GitHub Release → Actions runs publish job → check pypi.org for new version.

## Code Examples

### Complete CI/CD workflow with lint, test matrix, and deploy

> Full script: [complete-ci-cd-workflow-with-lint-test-matrix-and-.yml](scripts/complete-ci-cd-workflow-with-lint-test-matrix-and-.yml) (60 lines)

```yaml
# .github/workflows/ci.yml
# Input:  Python project with pyproject.toml or requirements.txt
# Output: Linted, tested, and deployed application
name: Python CI/CD
on:
# ... (see full script)
```

### PyPI library publish workflow with trusted publishing

> Full script: [pypi-library-publish-workflow-with-trusted-publish.yml](scripts/pypi-library-publish-workflow-with-trusted-publish.yml) (36 lines)

```yaml
# .github/workflows/publish.yml
# Input:  Python library with pyproject.toml
# Output: Published package on pypi.org
name: Publish to PyPI
on:
# ... (see full script)
```

### Django CI with PostgreSQL service container

> Full script: [django-ci-with-postgresql-service-container.yml](scripts/django-ci-with-postgresql-service-container.yml) (36 lines)

```yaml
# .github/workflows/django.yml
# Input:  Django project with PostgreSQL database
# Output: Tested Django application with database integration tests
name: Django CI
on:
# ... (see full script)
```

## Anti-Patterns

### Wrong: Using floating Python version

```yaml
# ❌ BAD — 'python3' or '3.x' resolves to different versions over time
steps:
  - uses: actions/setup-python@v5
    with:
      python-version: '3.x'
```

### Correct: Pinning exact Python minor version

```yaml
# ✅ GOOD — deterministic, same version every run
steps:
  - uses: actions/setup-python@v5
    with:
      python-version: '3.12'
```

### Wrong: Installing dependencies without pinned versions

```yaml
# ❌ BAD — unpinned dependencies change between runs, causing flaky builds
steps:
  - run: pip install django pytest requests
```

### Correct: Using requirements file with pinned versions

```yaml
# ✅ GOOD — deterministic install from lockfile
steps:
  - run: pip install -r requirements.txt
  # requirements.txt contains: django==5.1.2, pytest==8.3.3, etc.
```

### Wrong: Using PyPI password in secrets

```yaml
# ❌ BAD — password-based auth is deprecated; tokens can leak in logs
- run: twine upload dist/*
  env:
    TWINE_USERNAME: __token__
    TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
```

### Correct: Using OIDC trusted publishing

```yaml
# ✅ GOOD — no secrets needed; OIDC proves workflow identity to PyPI
permissions:
  id-token: write
steps:
  - uses: pypa/gh-action-pypi-publish@release/v1
  # Configure trusted publisher on PyPI first
```

### Wrong: Running all Python versions even after first failure

```yaml
# ❌ BAD — fail-fast: true (default) cancels all jobs on first failure, wasting context
strategy:
  matrix:
    python-version: ['3.10', '3.11', '3.12']
  # fail-fast defaults to true
```

### Correct: Setting fail-fast to false for full matrix coverage

```yaml
# ✅ GOOD — all versions run to completion, giving complete compatibility picture
strategy:
  fail-fast: false
  matrix:
    python-version: ['3.10', '3.11', '3.12', '3.13']
```

## Common Pitfalls

- **Python version as number, not string**: YAML interprets `3.10` as `3.1` (float). Fix: always quote Python versions as strings: `'3.10'`. [src1]
- **Cache not restoring**: `cache: 'pip'` needs a dependency file (requirements.txt, pyproject.toml). Fix: ensure the file exists and specify `cache-dependency-path` if not at repo root. [src2]
- **Different behavior on Windows**: Backslash paths, different line endings, case-insensitive imports. Fix: add `runs-on: windows-latest` to matrix if you need Windows support. [src4]
- **tox-gh-actions version mismatch**: `tox-gh-actions` maps GitHub Actions Python version to tox envs. Fix: ensure `tox.ini` `[gh-actions]` section matches your matrix versions. [src4]
- **pytest not finding tests**: Default test discovery looks for `test_*.py` files. Fix: configure `testpaths` in `pyproject.toml` or pass explicit paths to pytest. [src7]
- **Import errors in CI**: Package not installed in editable mode. Fix: use `pip install -e ".[dev]"` to install your package with dev dependencies. [src1]

## Diagnostic Commands

```bash
# Check installed Python version
python --version

# List installed packages with versions
pip list

# Check pip cache location
pip cache dir

# Verify all dependencies are satisfied
pip check

# Run pytest with verbose output
pytest -v --tb=long

# List all tox environments
tox -l

# Validate workflow YAML locally (requires actionlint)
actionlint .github/workflows/ci.yml

# Check workflow run status via CLI
gh run list --workflow=ci.yml --limit=5
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| actions/setup-python@v5 | Current | Dropped Python 2.7/3.5/3.6 support | Update python-version to 3.9+ |
| actions/setup-python@v4 | Deprecated | — | Replace @v4 with @v5 |
| Python 3.13 | Current | Removed `imghdr`, `cgi` modules | Check deprecation warnings |
| Python 3.12 | Active | f-string improvements, `type` keyword | Recommended for production |
| Python 3.11 | Security fixes until Oct 2027 | — | Stable, well-supported |
| Python 3.10 | Security fixes until Oct 2026 | — | Minimum recommended version |
| Python 3.9 | Security fixes until Oct 2025 | — | Plan upgrade to 3.10+ |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Project is hosted on GitHub | Project is on GitLab | GitLab CI/CD |
| Need matrix testing across Python versions | Need GPU access for ML training | Self-hosted runners or cloud ML platforms |
| Want native GitHub integration (PR checks) | Need complex DAG pipelines | Apache Airflow or Prefect |
| Publishing to PyPI | Need 100+ concurrent test runners | Distributed test runners (e.g., pytest-xdist on self-hosted) |
| Django/Flask/FastAPI application CI | Need private PyPI with complex auth | JFrog Artifactory or AWS CodeArtifact |

## Important Caveats

- Python version `3.10` without quotes is parsed as `3.1` by YAML. Always quote: `'3.10'`.
- `setup-python` caches the pip download cache, not the virtual environment itself. Re-installation still occurs but packages are fetched from cache.
- Free tier GitHub Actions provides 2,000 minutes/month for private repos. Public repos get unlimited minutes.
- Service containers (PostgreSQL, Redis) only work on Linux runners (`ubuntu-latest`), not Windows or macOS.
- OIDC trusted publishing requires configuring the trusted publisher on PyPI before the first publish.

## Related Units

- [GitHub Actions: Node.js CI/CD Pipeline](/software/devops/github-actions-nodejs/2026)
- [GitHub Actions: Docker Build and Push](/software/devops/github-actions-docker/2026)
- [GitLab CI: Basic Pipeline](/software/devops/gitlab-ci-pipeline/2026)
