How Do I Cache pip Dependencies in GitHub Actions?
Caching is off by default on the Python setup action. Set cache to pip, pipenv, or poetry and point cache-dependency-path at the file that changes.
Last verified:
Answer
Caching pip dependencies in GitHub Actions takes one input, because caching is off by default on the Python setup action and a workflow that never sets it re-downloads every package on every run. Set cache to pip, pipenv, or poetry, and point cache-dependency-path at the requirements or lock file whose contents should invalidate the entry.
On WarpBuild runners the action to use is WarpBuilds/setup-python@v6, a drop-in replacement for the upstream Python setup action that accepts the same inputs and sends the cache traffic to WarpBuild Cache instead of GitHub Actions Cache.
- uses: WarpBuilds/setup-python@v6
with:
python-version: '3.12'
cache: pip
cache-dependency-path: requirements.txtTwo inputs carry the whole behavior:
cachenames the package manager whose artifact directory gets stored and restored. Valid values arepip,pipenv, andpoetry. Leave it unset and nothing is cached.cache-dependency-pathnames the file whose hash goes into the cache key. Leave it unset and the action looks for the default dependency file for the package manager you named, which is correct only when that file sits at the repository root and there is exactly one of them.
| Package manager | cache value | File to hash | What gets stored |
|---|---|---|---|
| pip | pip | requirements.txt | pip download and wheel cache directory |
| Pipenv | pipenv | Pipfile.lock | Pipenv virtualenv and download directories |
| Poetry | poetry | poetry.lock | Poetry cache directory |
The default-off behavior is specific to Python. The Go setup action caches by default and keys on the hash of go.mod, so a Go workflow that never mentions caching still gets it, and a Python workflow copied from the same repository does not. That mismatch is the single most common reason a Python job keeps installing the same packages from scratch on every run while a neighboring Go job does not. The per-language table of defaults and inputs is in the WarpBuild setup actions documentation.
One thing the input does not do: it does not cache installed packages. The restore removes the download and, on a hit, the wheel build. The pip install step still runs on every job. Language-level sizing and matrix layout are covered on the Python on GitHub Actions solution page.
Detail
Point cache-dependency-path at the file that changes
The cache key is only as good as the file behind it. Three layouts break the default:
- Requirements outside the root. A service in
services/api/requirements.txtwith nothing at the root means the action finds no file to hash. Setcache-dependency-path: services/api/requirements.txt. - Split requirements files. A repository with
requirements.txtandrequirements-dev.txthashes only what you name. If the job installs both, name both, one per line, so a change to either one produces a new key. - Constraints and extras. A
pyproject.tomlthat pins nothing and apoetry.lockthat pins everything are different keys. Hash the lock file rather than the manifest whenever a lock file exists.
The path input accepts globs and multiple lines:
- uses: WarpBuilds/setup-python@v6
with:
python-version: '3.12'
cache: pip
cache-dependency-path: |
requirements.txt
requirements-dev.txt
services/*/requirements.txtThe failure mode of an over-broad glob is the opposite of the failure mode of a missing one. Hash every requirements.txt in a monorepo and any team's dependency bump busts every other team's cache. Hash too few and a real dependency change restores a stale entry, which pip then has to reconcile at install time. Start narrow, at the files the job actually installs from.
Follow-on cost 1: wheels that build from source on every miss
A cache miss on a pure-Python project costs a download. A cache miss on a project with compiled dependencies costs a compile. Packages that ship prebuilt wheels for your interpreter version and platform install in seconds; packages that do not ship a matching wheel are downloaded as source distributions and built on the runner, which pulls in a compiler toolchain and turns a 20 second install into a multi-minute one.
This shows up most sharply on two axes. New interpreter releases arrive before every project has published wheels for them, so a matrix leg on the newest Python builds from source while the others do not. And a Linux ARM64 leg can hit source builds for a package whose x64 wheel exists, because the maintainer publishes only one architecture.
The pip cache directory covers this, which is why it is worth enabling even on projects that install quickly. pip stores the wheel it built locally, so the compile is paid once per key rather than once per job. That makes the cost of a busted key higher than it looks, because the run that misses pays for the compile as well as for the bytes.
If the compile is unavoidable and lands on every run anyway, the next lever is the machine image rather than the package cache. Snapshot runners capture a runner VM mid-workflow and boot later jobs from that snapshot, which moves toolchain installs and long source builds out of the job entirely. Snapshot runners sit alongside remote Docker builders, GitHub Actions observability, an MCP server, and the Action Debugger in the WarpBuild product surface. The arithmetic for that trade is at the end of this page.
Follow-on cost 2: virtualenv layouts the default paths do not cover
The second cost is quieter. The setup action knows where pip, Pipenv, and Poetry keep their own directories. It does not know where your workflow put a virtualenv.
A job that runs python -m venv .venv and installs into it gets a correct pip cache and an uncached .venv. Every run restores the downloads and then rebuilds the environment from them. That is still a win over no cache, and it is roughly half the win available.
Two shapes fix it:
- Let the tool own the environment. Poetry and Pipenv keep environments in directories the setup action already handles. Removing a hand-rolled
venvstep is usually less work than caching around it. - Cache the environment explicitly. Add a
WarpBuilds/cache@v1step keyed on the same lock file hash. That action is a drop-in replacement foractions/cache@v4and takes the same inputs, documented in WarpBuild caching.
A virtualenv cache carries one hazard the download cache does not. A virtualenv holds absolute paths and compiled artifacts tied to the interpreter that created it. Put the interpreter version in the key, and never restore one across runner images.
The full workflow
name: test
on:
push:
branches: [main]
pull_request:
jobs:
pytest:
runs-on: warp-ubuntu-latest-x64-4x
strategy:
fail-fast: false
matrix:
python-version: ['3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4
- uses: WarpBuilds/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: |
requirements.txt
requirements-dev.txt
- name: Restore project virtualenv
id: venv-cache
uses: WarpBuilds/cache@v1
with:
path: .venv
key: venv-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('requirements.txt', 'requirements-dev.txt') }}
- name: Create virtualenv
if: steps.venv-cache.outputs.cache-hit != 'true'
run: |
python -m venv .venv
.venv/bin/pip install -r requirements.txt -r requirements-dev.txt
- run: .venv/bin/pytest -qThe cache-hit output is what makes the second layer pay. That output reads true only on an exact key match, so the install step is skipped on a hit and runs on a partial match, which is the behavior you want when a restore-keys prefix pulled in a near miss.
What the key covers, and why a macOS entry never restores on Linux
A WarpBuild cache entry is scoped to three things: the key, the version, and the branch. The key is what you write. The version is a hash covering the compression tool and the list of cached paths, so an entry written on warp-macos-latest-arm64-6x cannot restore on warp-ubuntu-latest-x64-4x even when both jobs compute the same key string. The branch scope means a feature branch reads entries written on its base branch but not the reverse.
For a Python matrix this has one practical consequence. A matrix over ['3.11', '3.12', '3.13'] on one runner image produces three entries, one per interpreter, and each one warms independently. The first run after adding a version is a full cold install for that leg only.
One platform note. WarpBuild caching is not supported on Windows runners. A cross-platform Python matrix should expect the Windows leg to install cold each time and should size that leg on that basis. The caching limitation is stated in the WarpBuild caching documentation.
What the cache costs
Cache work bills separately from runner minutes on hosted runners and is free on BYOC:
| Item | Hosted rate | Unit | BYOC |
|---|---|---|---|
| Cache storage | $0.20 | per GB-month | Free |
| Cache write, restore, or list | $0.0001 | per operation | Free |
| Snapshot restore | $0.04 | per job | Free |
| Snapshot storage | $0.025 | per snapshot-hour | Free |
Worked model for a mid-sized Python repository. Take a 600 MB pip cache, 3,000 jobs per month, one restore per job, and 40 writes per month from dependency bumps.
- Storage: 0.6 GB x $0.20 = $0.12 per month.
- Operations: (3,000 restores + 40 writes) x $0.0001 = $0.30 per month.
- Cache bill: $0.42 per month.
Now the minute side. Plug in your own step durations from the job log; the numbers below are placeholders for the arithmetic. Say the cold install takes 95 seconds and the warm restore plus install takes 20 seconds. That is 75 seconds per job, or 3,750 minutes across 3,000 jobs.
| Line | Rate per minute | 3,750 minutes | Source |
|---|---|---|---|
| warp-ubuntu-latest-x64-4x (4 vCPU, 16 GB) | $0.008 | $30.00 | WarpBuild pricing page |
| GitHub-hosted 4-core Linux larger runner (4 vCPU, 16 GB) | $0.012 | $45.00 | GitHub list price, checked on 2026-08-13 |
GitHub list prices come from the GitHub Actions minute multipliers reference, checked on 2026-08-13. On the WarpBuild line, $30.00 of runner minutes comes off the bill against a $0.42 cache bill, which is the shape of the trade at almost any repository size: cache storage and operations are small next to the minutes they remove.
Run the same arithmetic against your own durations before deciding anything. Every rate above is on the WarpBuild pricing page.
When to reach for a snapshot instead
If the install stays slow after the cache is warm, because a source build lands on every run or the toolchain itself takes minutes to install, price a snapshot against it.
At 3,000 jobs per month, snapshot restores cost 3,000 x $0.04 = $120.00, and one snapshot held for a 730 hour month costs 730 x $0.025 = $18.25. That is $138.25 per month, or about $0.046 per job. At $0.008 per minute on warp-ubuntu-latest-x64-4x, a snapshot pays for itself when it removes more than about 5.8 minutes of setup per job. Below that threshold, the pip cache alone is the cheaper instrument.
Related Questions
Does the Python setup action cache pip by default?
No. Caching is off by default on the Python setup action and you turn it on with the cache input, set to pip, pipenv, or poetry. This differs from the Go setup action, where caching is on by default and keyed on the hash of go.mod. On WarpBuild runners, WarpBuilds/setup-python@v6 routes those reads and writes to WarpBuild Cache with no other workflow change. The full input list is in the setup actions documentation.
How long does a cached pip entry last?
A cache entry expires after 7 days of last use, and you can delete it earlier from the action run or the WarpBuild console. An entry that is restored daily keeps resetting that window, so an active branch holds its pip cache indefinitely while a merged branch drops out about a week later. The answer on how long GitHub Actions caches last covers the eviction path in more detail.
What does caching pip dependencies cost?
On WarpBuild hosted runners, cache storage bills at $0.20 per GB-month and each write, restore, or list operation bills at $0.0001. Both are free on BYOC. A 600 MB pip cache restored on 3,000 jobs per month with 40 writes bills $0.12 in storage and $0.30 in operations, or $0.42 per month. Full rates are on the WarpBuild pricing page.
Should I cache the pip download cache or the installed virtualenv?
Cache the pip download cache first, because that is what the setup action handles for you and it removes the network and wheel-build work. Add a second cache step for a virtualenv only when the install step itself is the slow part and the environment lives at a path you control. The Python on GitHub Actions solution page walks through both layouts.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.