Skip to content
Latchkey LogoLatchkey home

actions/setup-python: version ranges, cache keys that go cold, and free-threaded builds

actions/setup-python installs a CPython, PyPy or GraalPy version, puts it on PATH, and can cache pip, pipenv or poetry dependencies keyed on your dependency files. In v7.1.0, the current release, its cache keys contain the full installed Python version and, since v6.3.0, the Linux distribution, so a change to either starts the cache cold.

A setup-python step is usually two lines, python-version and cache: pip, and both hide decisions. The version you write is a range the action resolves against what the runner already has, and the cache key is built from the result rather than from what you wrote.

That is how a cache goes cold with no change to your workflow: the runner image ships a new Python patch, the key changes with it, and the next run installs from scratch. v6.3.0 did the same thing once to every Linux cache when it added the distribution to the key.

The setup-python cache key split into segments, with a table of what pip, pipenv and poetry save
Every segment of the key is a way to miss. Only pip has a prefix fallback, and cache-hit reports true only on an exact match.

Inputs, and the version actually installed

The table is action.yml at the v7.1.0 tag. Three outputs come back: python-version, the version actually installed, which is what you want when the input was a range; python-path, the interpreter; and cache-hit. With update-environment left on, the action also updates PATH, PKG_CONFIG_PATH and pythonLocation; turn it off in a composite action that should not touch its caller's environment, and call python-path directly.

.github/workflows/ci.yml
permissions:
  contents: read
steps:
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    with:
      persist-credentials: false
  - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
    id: py
    with:
      python-version-file: .python-version
      cache: pip
  - run: pip install -r requirements.txt
  - run: echo "Installed ${{ steps.py.outputs.python-version }}"
InputDefaultWhat it does
python-versionRead from .python-versionExact version, range, x.y-dev, pypy3.10 or graalpy-24.0
python-version-fileNone.python-version, pyproject.toml, .tool-versions or Pipfile
cacheNonepip, pipenv or poetry
architectureThe host'sx86, x64 or arm64
check-latestfalseDownload a newer patch than the cached one when it exists
tokengithub.token on github.comFetches builds from actions/python-versions; never sent to a custom mirror
mirroractions/python-versions on raw.githubusercontent.comNew in v7.1.0: another source for CPython builds
mirror-tokenNoneNew in v7.1.0: sent verbatim as the Authorization header
cache-dependency-pathPer package manager, see belowFiles, globs or a list to hash for the key
update-environmenttrueSet pythonLocation and PATH for later steps
allow-prereleasesfalseFall back to a prerelease when no GA version matches an x.y range
freethreadedfalseInstall the free-threaded build
pip-versionNonePin pip to major[.minor][.patch]; CPython only

Choosing the version

If you set both inputs, the action warns "Both python-version and python-version-file inputs are specified, only python-version will be used." With neither, it reads .python-version from the repository, and failing that uses the Python already on PATH with a warning. The advanced usage guide is blunt that the PATH version varies between runners and can change unexpectedly.

The shape of the version decides the speed. A runner image keeps one patch of each supported minor, so 3.13 resolves to whatever patch is installed and sets up without a download. An exact 3.12.6 is reproducible, but if the image has moved on, the action downloads it, and on Windows that means an installer. That choice also decides your cache key, as the caching section shows.

x.y-dev takes the newest build of that minor including alphas, betas and release candidates, and an exact prerelease tag such as 3.14.0-alpha.1 installs that build. Semver ranges like >=3.9 <3.14 and 3.x work too.

Prereleases and free-threaded builds

allow-prereleases: true lets an x.y range fall back to a prerelease when no GA release of that minor exists yet, which keeps one matrix entry valid before and after the release. For CPython it applies only to the x.y form.

Free-threaded Python exists from 3.13. Ask for it with a t suffix, as in 3.13t, 3.13.1t or 3.14t-dev, or with freethreaded: true. The suffix is not semver, so a range such as >=3.13 needs the input instead. The python-version output carries the t, so the cache key of a free-threaded leg never collides with the standard build.

.github/workflows/test.yml
strategy:
  fail-fast: false
  matrix:
    python: ['3.13', '3.14', '3.14t']
steps:
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    with:
      persist-credentials: false
  - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
    with:
      python-version: ${{ matrix.python }}
      allow-prereleases: true
      cache: pip
  - run: python -m pip install -r requirements.txt && python -m pytest

Caching for pip, pipenv and poetry

The key is setup-python-<RUNNER_OS>-<arch>-<distro version>-<distro>-python-<installed version>-<manager>-<hash>, so an illustrative pip key on Ubuntu 24.04 reads setup-python-Linux-x64-24.04-Ubuntu-python-3.13.9-pip- followed by the hash. The version segment is the resolved version, patch included. With python-version: '3.13', the day the runner image moves to a new patch every key changes and every cache misses once. Pinning the patch keeps the key stable, at the cost of a download whenever the image has moved past it.

pip saves the pip cache directory and hashes /requirements.txt, falling back to /pyproject.toml. It is the only mode with a prefix fallback, and cache-hit reports false when that fallback restored. pipenv saves the virtualenvs directory and hashes /Pipfile.lock; poetry saves each project's virtualenvs path, plus .venv for in-project environments, and hashes /poetry.lock. Both are exact-match only. All three save in a post step that runs only on success.

Poetry has to be on PATH before this step, because the action asks it for its configuration, and after restoring it runs poetry env use with the new interpreter. Whether cache: pip is worth anything on your project is measured on the pip and poetry caching page.

.github/workflows/ci.yml
steps:
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    with:
      persist-credentials: false
  - run: pipx install poetry
  - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
    with:
      python-version: '3.13'
      cache: poetry
  - run: poetry install
  - run: poetry run pytest

Majors that moved the cache key

Two of these changes moved cache keys: v6.3.0 added the distribution segment, so the upgrade itself started every Linux cache cold once. The pip-install input lived for one minor release before v7 removed it.

VersionReleasedWhat changed
v5.0.02023-12-06Runtime moved from node16 to node20
v6.0.02025-09-04Runtime moved to node24 (runner v2.327.1 or later); pip-version input; versions read from Pipfile; a missing cache directory became a warning
v6.1.02025-11-25pip-install input; GraalPy early-access and Windows builds
v6.3.02026-06-24RHEL support; Linux distribution added to cache keys
v7.0.02026-07-20ESM internals; pip-install removed; the versions manifest is validated and its fetch retried
v7.1.02026-10-22mirror and mirror-token inputs; clearer error messages

Versions not found and dependency files missing

There is no dedicated Learn page for these yet, so the fixes are here. Network failures pip hits after the step, in the install itself, are on pip read timeouts and 429s in CI.

MessageCauseFix
The version '3.8' with architecture 'x64' was not found for Ubuntu 24.04.No build in the versions manifest for that version, OS and architecture, or before v7.0.0 a manifest fetch that failed quietlyCheck the manifest URL the error prints; move to v7 for the retry
The specified python version file at: <path> doesn't exist.The path is wrong, or checkout has not runFix the path relative to the workspace
No file in <dir> matched to [/requirements.txt or /pyproject.toml] for pip.cache is set and no dependency file exists, often in a reusable workflowSet cache-dependency-path, or drop cache
Unable to locate executable file: poetry.cache: poetry ran before Poetry was installedInstall Poetry in an earlier step
Invalid pip-version "<value>". Please specify a version in the format major[.minor][.patch].pip-version is not a plain versionUse 25, 25.1 or 25.0.1

Key takeaways

  • Set python-version or python-version-file; without either the action falls back to .python-version, then to whatever is on PATH.
  • The cache key holds the installed patch version and, since v6.3.0, the Linux distribution, so either change misses once.
  • Only pip has a prefix fallback; pipenv and poetry restore on an exact key or not at all.
  • Free-threaded builds start at 3.13: use the t suffix for a single version and freethreaded: true for a range.

Frequently asked questions

What does "version with architecture x64 was not found" mean in setup-python?
The action found no build for that version, operating system and architecture in the actions/python-versions manifest, the same failure older releases worded as "Version X with arch x64 not found". In issue 1319 it was a temporary fetch problem rather than a missing build, which is why v7.0.0 validates the manifest and retries the fetch. If it persists, the build does not exist for that runner image.
Does cache: pip cache the installed packages?
No. It restores pip's own cache directory, so pip install still runs and still installs every package; it downloads and builds less. Issue 330 asking for the installed packages to be cached is still open; the poetry and pipenv modes are the ones that save an environment.
Why does poetry install use the wrong Python version after setup-python?
Poetry creates environments with the Python it was installed with, and the documented order installs Poetry before setup-python (issue 846). With cache: poetry the action runs poetry env use against the new interpreter after restoring; without it, run poetry env use "${{ steps.py.outputs.python-path }}" yourself before poetry install.
What happened to the pip-install input?
It arrived in v6.1.0 and was removed in v7.0.0, after issue 1251 pointed out that it installed packages into the runner-level Python environment, which a virtual environment may not see. Replace it with an ordinary run: python -m pip install ... step, or a virtual environment, after setup-python.

References

A pip cache keyed the way you choose. Latchkey cache-action, in two steps. Start free → 30-day trial · No credit card