actions/setup-python@v5 with cache: 'pip' and pip install -r requirements.txt.requirements.txt -- floating versions cause non-deterministic builds.'3.12') -- never use just python3 which varies across runner images.pip install -r requirements.txt with pinned versions -- avoid floating version specifiers in CI.actions/setup-python@v5) -- never use @main in production.| Setting | Value / Command | Notes |
|---|---|---|
| Workflow file location | .github/workflows/*.yml | Any YAML file in this directory |
| 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 deps | cache: 'pip' on setup-python | Requires requirements.txt or pyproject.toml |
| Install deps | pip install -r requirements.txt | Or pip install -e ".[dev]" |
| 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 . | 10-100x faster than flake8 |
| Type checking | mypy --strict src/ | Optional but recommended |
| Format check | ruff format --check . | Non-destructive in CI |
| Matrix strategy | matrix: python-version: ['3.10', '3.12', '3.13'] | Runs in parallel |
| Publish to PyPI | pypa/gh-action-pypi-publish@release/v1 | OIDC trusted publishing |
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
Create .github/workflows/ci.yml in your repository. [src1]
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 4 parallel matrix jobs.
Run linters before tests to fail fast. [src4]
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 .
Verify: Introduce a lint error → push → workflow fails at ruff check step.
The cache: 'pip' option caches the pip download directory. [src2]
- 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" on second run.
Generate coverage reports and upload as artifacts. [src7]
- 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/
Verify: After workflow completes → Artifacts section shows coverage-report.
Use OIDC trusted publishing -- no secrets needed. [src5]
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 → check pypi.org for new version.
# .github/workflows/ci.yml
name: Python CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
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 .
test:
runs-on: ubuntu-latest
needs: lint
strategy:
fail-fast: false
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 }}
cache: 'pip'
- run: pip install -r requirements.txt
- run: pytest --tb=short -q --junitxml=report.xml --cov=src --cov-report=xml
deploy:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -r requirements.txt
- name: Deploy
run: echo "Deploy your application here"
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
# .github/workflows/django.yml
name: Django CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -r requirements.txt
- run: python manage.py test --verbosity=2
# ❌ BAD — '3.x' resolves to different versions over time
- uses: actions/setup-python@v5
with:
python-version: '3.x'
# ✅ GOOD — deterministic, same version every run
- uses: actions/setup-python@v5
with:
python-version: '3.12'
# ❌ BAD — password-based auth is deprecated
- run: twine upload dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
# ✅ GOOD — no secrets needed; OIDC proves workflow identity to PyPI
permissions:
id-token: write
steps:
- uses: pypa/gh-action-pypi-publish@release/v1
# ❌ BAD — unpinned dependencies change between runs
- run: pip install django pytest requests
# ✅ GOOD — deterministic install from lockfile
- run: pip install -r requirements.txt
# requirements.txt: django==5.1.2, pytest==8.3.3
3.10 as 3.1 (float). Fix: always quote: '3.10'. [src1]cache: 'pip' needs a dependency file. Fix: ensure requirements.txt exists and specify cache-dependency-path. [src2]runs-on: windows-latest to matrix. [src4][gh-actions] section in tox.ini matches matrix. [src4]test_*.py. Fix: configure testpaths in pyproject.toml. [src7]pip install -e ".[dev]". [src1]# 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
actionlint .github/workflows/ci.yml
# Check workflow run status via CLI
gh run list --workflow=ci.yml --limit=5
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| actions/setup-python@v5 | Current | Dropped Python 2.7/3.5/3.6 | 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 |
| 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 for ML training | Self-hosted runners or cloud ML |
| Want native GitHub integration | Need complex DAG pipelines | Apache Airflow or Prefect |
| Publishing to PyPI | Need 100+ concurrent runners | Distributed test runners |
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. Re-installation still occurs.