Using uv in GitHub Actions means swapping pip install -r requirements.txt for uv sync --locked, adding one official action, and turning on a cache flag. The payoff isn’t the “10-100x faster” number uv’s own docs advertise for cold installs — in a real CI job with a warm cache, the honest number is closer to 1.4x. Still worth it, but worth knowing which number is real before you promise your team a 10x pipeline.
Setting Up uv in GitHub Actions: What setup-uv Installs
The official astral-sh/setup-uv action downloads the uv binary, puts it on PATH, and — if you ask it to — manages a cache. It does not manage your Python interpreter by default; that’s a separate step. Pin it to a commit SHA, not a floating version tag, the same way you’d pin any third-party action that runs with repo access:
- name: Install uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
As of this writing v10.3.0 (released 2026-10-09) is the latest tag. Check the releases page before you copy this into a workflow — this action ships new versions often, and GitHub Actions security best practices already cover why floating tags on third-party actions are a supply-chain risk, not just a staleness problem.
Installing Python: Two Valid Paths
You have two options, and they’re not interchangeable.
Let uv manage Python:
- name: Set up Python
run: uv python install
This respects a .python-version file or a requires-python pin in pyproject.toml, and it’s the simpler option if you don’t need anything else from actions/setup-python.
Use the official setup-python action instead:
- uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
The uv docs themselves note this path gets faster caching in some cases. If your workflow already depends on setup-python outputs elsewhere, there’s no strong reason to rip it out just to use uv’s own installer. The GitHub Actions testing guide covers the matrix-build syntax in more depth if you’re setting this up for the first time.
For matrix builds across Python versions, pass the version through setup-uv directly instead of juggling two actions:
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
python-version: ${{ matrix.python-version }}
Caching: What You Get for Free, What You Still Configure
enable-cache: true on setup-uv persists uv’s package cache across runs using GitHub’s own Actions Cache, keyed against your lockfile. That’s the whole setup for most projects — this replaces the cache: pip line you’d have had on actions/setup-python before, and the GitHub Actions caching guide covers the mechanics of how that cache-key invalidation actually works if you want the full picture.
⚠️ Important: on self-hosted runners, that cache directory grows unbounded across runs since there’s no ephemeral VM to reset it. The docs recommend pointing UV_CACHE_DIR inside your workspace and running uv cache prune --ci as a cleanup step if you’re not on GitHub-hosted runners.
Running uv in GitHub Actions: The Actual Job
- name: Install the project
run: uv sync --locked --all-extras --dev
- name: Run tests
run: uv run pytest tests
uv sync --locked fails the build if uv.lock is out of sync with pyproject.toml. It does not silently re-resolve. That’s the flag you want when running uv in GitHub Actions — a surprise dependency bump should break the pipeline loudly, not pass quietly with different versions than what’s pinned.
The Real Numbers, Not the Marketing Ones
A real migration writeup from the CPython blurb tool (PR python/blurb#32) gives honest before/after timing instead of a synthetic benchmark:
| Job | No cache | With cache |
|---|---|---|
| Linting (pre-commit) | 60s → 37s (1.62x) | 11s → 11s (no change) |
| Testing (tox) | 2m 0s → 1m 26s (1.40x) | 1m 58s → 1m 22s (1.44x) |
That’s the number to plan around — 1.4-1.6x on a real job, not the 10-100x cold-install figure from uv’s own benchmark page. The 10-100x claim is real too, but it’s measured against pip’s cold-cache dependency resolution, which most CI jobs with any caching already avoid hitting on every run.
What Breaks When You Migrate from tox + pip
If your test runner is tox, migrating isn’t just swapping the install command. The real gotcha the writeup above flags: uv environments don’t have pip installed by default. Any tox.ini line that shells out to pip install -r requirements.txt breaks. Two fixes:
- Replace pip-based installs with tox’s own
deps = -r requirements.txtdeclaration, which uv’s tox integration (tox-uv) understands natively. - Or add
uv_seed = Trueto the relevanttox.inienvironment to restorepip,setuptools, andwheelif something in your pipeline genuinely still needs them.
Also remove any leftover cache: pip line from a setup-python step you’re keeping around — uv handles its own caching, and a redundant pip cache just adds a cache-restore step that does nothing.
Frequently Asked Questions
Q: Does uv replace actions/setup-python entirely?
A: Not necessarily. uv python install can manage the interpreter on its own, but actions/setup-python still works alongside uv and some teams keep it for faster caching on the Python binary itself. Either path is valid.
Q: Why pin setup-uv to a commit SHA instead of a version tag?
A: Same reason as any third-party action with write access to your workflow — a tag can be moved to point at different code later, a SHA can’t. This matters more for third-party actions than GitHub’s own first-party ones, but setup-uv is third-party.
Q: Does uv sync --locked fail the build if the lockfile is stale?
A: Yes — that’s the point of using --locked specifically in CI instead of plain uv sync, which would silently re-resolve and potentially install different versions than what’s pinned.
Q: Is the 10-100x speed claim fake?
A: No, it’s real for the specific case it measures — a cold install with no cache. Most CI pipelines with caching already enabled won’t see that number; expect 1.4-1.6x on a warm-cache job based on real migration data.
Quick Summary:
– astral-sh/setup-uv, SHA-pinned, with enable-cache: true is the whole caching setup for most projects
– uv sync --locked fails loudly on a stale lockfile instead of silently re-resolving — use it in CI, not plain uv sync
– Real CI speedup from a documented migration: 1.4-1.6x, not the 10-100x cold-install marketing number
– uv environments don’t ship pip — tox.ini lines that shell out to pip install need uv_seed = True or a deps = rewrite
– Self-hosted runners need manual cache pruning (uv cache prune --ci) since there’s no ephemeral VM to reset the cache directory
Check your current setup-python + pip install step against the uv in GitHub Actions version above — if your lockfile is already in the repo, the migration is a 10-minute change, not a project.
Related guides
- Getting Started with GitHub Actions: Your First CI/CD Pipeline
- A Comparative Overview of CI/CD Tools in Azure, GCP, and AWS
- Automating Terraform Deployments with GitLab CI/CD: GCP
- CI/CD Tools Showdown: A Comprehensive Comparison
- Comparison of popular CI/CD (Continuous Integration/Continuous Deployment) tools
- Deploying AWS Resources with GitLab CI/CD and Terraform
- Boto3 Retry Config: Why AWS Scripts Fail Silently