Skip to content
Français
CI/CD & Automatisation medium

Python and pip caching in GitHub Actions

30 min de lecture

Read this page in French

Python projects can have significant dependency install times, especially with packages such as numpy, pandas or tensorflow that require compilation. The cache brings that time down from several minutes to a few seconds.

What you will learn

  • Enable the pip cache built into setup-python
  • Cache Poetry and Pipenv depending on your dependency manager
  • Configure actions/cache for fine-grained control of the paths
  • Cache a complete virtual environment to skip the install altogether
  • Avoid the traps: a cache invalidated by a Python upgrade, a double cache

The built-in cache of setup-python

The simplest method uses the cache built into actions/setup-python:

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'
cache: 'pip' # Enables the pip cache
- run: pip install -r requirements.txt

With Poetry

If you manage your dependencies with Poetry, tell setup-python about it: the cache will then key off poetry.lock.

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'
cache: 'poetry'
- run: |
pip install poetry
poetry install

With Pipenv

The same principle for Pipenv: the cache then follows Pipfile.lock.

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'
cache: 'pipenv'
- run: |
pip install pipenv
pipenv install --dev

Manual caching with actions/cache

For more control, use actions/cache directly:

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
pip-${{ runner.os }}-
- run: pip install -r requirements.txt

The cache paths per manager

If you configure actions/cache by hand, the path: depends on the manager and on the operating system. This table gives the locations to cache.

ManagerLinux pathmacOS pathWindows path
pip~/.cache/pip~/Library/Caches/pip~\AppData\Local\pip\Cache
Poetry~/.cache/pypoetry~/Library/Caches/pypoetry%APPDATA%\pypoetry\Cache
Pipenv~/.cache/pipenv~/Library/Caches/pipenv%USERPROFILE%\.pipenv

Caching the virtual environment

For maximum gains, cache the complete virtual environment:

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'
- name: Cache virtualenv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: cache-venv
with:
path: .venv
key: venv-${{ runner.os }}-${{ hashFiles('**/requirements*.txt') }}
- name: Create venv and install deps
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
- name: Run tests
run: |
source .venv/bin/activate
pytest

Gain: the install is skipped entirely when the cache exists.

A complete Python workflow

name: Python CI
on: [push, pull_request]
# No rights by default: the job asks for the minimum
permissions: {}
jobs:
test:
runs-on: ubuntu-24.04
permissions:
contents: read
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: |
requirements.txt
requirements-dev.txt
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run linters
run: |
ruff check .
mypy .
- name: Run tests
run: pytest --cov=src --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./coverage.xml

Caching with Poetry (advanced)

name: Python CI with Poetry
on: [push, pull_request]
permissions: {}
jobs:
test:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'
- name: Install Poetry
uses: snok/install-poetry@a783c322200f0519c7926aa6faa857c4e23e9263 # v1.4.2
with:
version: '1.8.2'
virtualenvs-create: true
virtualenvs-in-project: true
- name: Cache Poetry virtualenv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .venv
key: poetry-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
poetry-${{ runner.os }}-
- name: Install dependencies
run: poetry install --no-interaction
- name: Run tests
run: poetry run pytest

The specific optimisations

Packages with native dependencies

For numpy, scipy and pandas, which carry compiled dependencies:

- name: Cache pip wheels
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cache/pip
~/.local/lib/python*/site-packages
key: pip-wheels-${{ runner.os }}-${{ hashFiles('**/requirements*.txt') }}

Pre-commit hooks

Pre-commit environments are slow to rebuild. Caching them makes the hook almost instant from the second run onwards.

- name: Cache pre-commit
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
- run: pre-commit run --all-files

The common mistakes

A cache invalidated by a Python upgrade

A pip cache depends on the Python version. If the key does not include it, an upgrade restores packages compiled for the previous one, and everything breaks.

# ❌ The key does not change when Python changes
key: pip-${{ hashFiles('requirements.txt') }}
# ✅ Include the Python version
key: pip-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}

A conflict with the setup-python cache

Stacking the cache: of setup-python and an actions/cache on the same path produces two managers stepping on each other. Keep only one.

# ❌ A double cache, with unpredictable behaviour
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
cache: 'pip'
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ~/.cache/pip
# ✅ Use one or the other
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
cache: 'pip'

Key points

  • The built-in cache of setup-python (cache: 'pip') is enough for most projects, in one line.
  • For Poetry or Pipenv, name the right manager: cache: 'poetry', cache: 'pipenv'.
  • actions/cache gives you control over the paths: useful for compiled wheels or pre-commit hooks.
  • Caching the complete virtualenv skips the install outright, the maximum gain.
  • Include the Python version in the cache key, otherwise a Python upgrade restores an incompatible cache.
  • Never stack the cache: of setup-python and an actions/cache on the same path: pick one.

Next steps

  • Java cache: the same reasoning about keys and paths applied to Maven and Gradle, useful as soon as a repository mixes several languages.
  • Concurrency: the complementary lever, which avoids paying for several Python installs on commits that are already obsolete.
  • Introduction to runners: what explains why the machine starts blank on every run, and therefore why the pip cache exists.

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn