uv and Rye Python Toolchain Testing in CI: A Complete Guide
Python's packaging ecosystem has a reputation for being painful. Multiple tools, slow installs, environment drift, and lock file formats that differ between tools — it adds friction to CI pipelines that should be invisible. uv and Rye are two tools that aim to fix this. Both are written in Rust, both are dramatically faster than pip and pip-tools, and both bring reproducible environments to Python projects. This guide covers how to use them to build fast, reliable CI testing pipelines.
uv: The Fast Python Installer
uv is a Python package installer and resolver from Astral (the team behind Ruff). It is a drop-in replacement for pip, pip-tools, and virtualenv. uv resolves and installs packages 10-100x faster than pip, uses a global content-addressable cache to avoid re-downloading packages, and produces deterministic lock files.
Installing uv
curl -LsSf https://astral.sh/uv/install.sh | shOr on macOS with Homebrew:
brew install uvProject Setup with uv
Initialize a new project:
uv init my-project
cd my-projectThis creates a pyproject.toml with a minimal project structure. Add dependencies:
uv add pytest pytest-cov httpx
uv add --dev ruff mypyuv maintains a uv.lock file — a cross-platform lock file that pins every transitive dependency. Commit this file to version control.
Running Tests with uv
# Create virtual environment and install dependencies
uv <span class="hljs-built_in">sync
<span class="hljs-comment"># Run pytest through uv
uv run pytest tests/
<span class="hljs-comment"># Run with coverage
uv run pytest tests/ --cov=src --cov-report=xmluv run ensures the command runs inside the project's virtual environment without requiring you to activate it manually. This is important for CI — no source .venv/bin/activate required.
uv in GitHub Actions
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --frozen
- name: Run tests
run: uv run pytest tests/ --cov=src --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.xmlKey details:
setup-uvhandles installation and cachingenable-cache: truecaches the uv package cache between runsuv sync --frozeninstalls exactly what is inuv.lockwithout updating it — critical for reproducibilityuv python install 3.12pins the Python version
Testing Against Multiple Python Versions
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --frozen --python ${{ matrix.python-version }}
- name: Run tests
run: uv run pytest tests/This matrix runs your full test suite against three Python versions in parallel. uv's speed means the overhead per version is seconds, not minutes.
Rye: The All-in-One Python Project Manager
Rye takes a different position: it manages Python versions, virtual environments, dependencies, and scripts from a single tool. Think of it as Cargo for Python. Rye is opinionated about project structure and uses uv under the hood for package installation.
Installing Rye
curl -sSf https://rye.astral.sh/get | bashProject Setup with Rye
rye init my-project
cd my-project
rye add pytest pytest-cov
rye add --dev ruff mypyRye creates a pyproject.toml with [tool.rye] sections and manages a requirements.lock file. The workflow:
rye sync <span class="hljs-comment"># install/update dependencies
rye run pytest <span class="hljs-comment"># run pytest inside the managed environmentLike uv run, rye run executes in the project's virtual environment without manual activation.
Rye in GitHub Actions
name: Test with Rye
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rye
uses: eifinger/setup-rye@v4
with:
enable-cache: true
cache-prefix: rye-cache
- name: Sync dependencies
run: rye sync --no-dev
- name: Run tests
run: rye run pytest tests/ -v --tb=short
- name: Run type checking
run: rye run mypy src/
- name: Run linting
run: rye run ruff check src/ tests/The eifinger/setup-rye action installs Rye and handles caching. rye sync --no-dev installs production dependencies only; use rye sync to include dev dependencies.
Practical Patterns for Fast CI
Cache Strategy
Both uv and Rye benefit from caching the package download cache. The cache key should include the lock file so the cache invalidates when dependencies change:
- name: Cache uv packages
uses: actions/cache@v4
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: uv-${{ runner.os }}-With a warm cache, uv sync on a typical Python project with 20-30 dependencies takes 2-5 seconds instead of 30-60 seconds.
Separating Lint from Tests
Run linting and type checking as separate jobs that can fail independently:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync --frozen
- run: uv run ruff check src/ tests/
- run: uv run mypy src/
test:
runs-on: ubuntu-latest
needs: [] # run in parallel with lint
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- run: uv sync --frozen
- run: uv run pytest tests/ --cov=srcRunning lint and test jobs in parallel reduces total CI wall time.
Environment Variables and Secrets
Tests that hit real services or use API keys need environment variables. In GitHub Actions:
- name: Run integration tests
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: uv run pytest tests/integration/ -vFor local development with the same env vars, use a .env file and load it with python-dotenv or uv run --env-file .env pytest.
Splitting Unit and Integration Tests
Use pytest markers to separate fast unit tests from slower integration tests:
# tests/test_api.py
import pytest
@pytest.mark.unit
def test_validate_email():
assert validate_email("user@example.com") == True
@pytest.mark.integration
def test_api_endpoint():
# requires running server
response = client.get("/health")
assert response.status_code == 200In CI:
- name: Unit tests (fast)
run: uv run pytest tests/ -m unit --tb=short
- name: Integration tests
run: uv run pytest tests/ -m integration --tb=short
if: github.event_name == 'push' && github.ref == 'refs/heads/main'Run unit tests on every push, integration tests only on merges to main. This keeps PR feedback loops fast.
Migrating from pip/pip-tools to uv
If you have an existing project using requirements.txt or requirements.in, migration is straightforward:
# Install uv
curl -LsSf https://astral.sh/uv/install.sh <span class="hljs-pipe">| sh
<span class="hljs-comment"># Convert requirements.txt to uv
uv add $(<span class="hljs-built_in">cat requirements.txt <span class="hljs-pipe">| grep -v <span class="hljs-string">'^#' <span class="hljs-pipe">| grep -v <span class="hljs-string">'^$')
<span class="hljs-comment"># Or, keep requirements.txt and use uv as the installer
uv pip install -r requirements.txtFor CI, replace pip install -r requirements.txt with uv pip install -r requirements.txt and you immediately get faster installs without changing your project structure.
Comparing uv and Rye
| Feature | uv | Rye |
|---|---|---|
| Python version management | Yes (uv python) | Yes (rye pin) |
| Dependency management | Yes | Yes (uses uv) |
| Lock file | uv.lock | requirements.lock |
| Script runner | uv run | rye run |
| Build backend management | No | Yes |
| Opinionated project layout | No | Yes |
Choose uv if you want maximum flexibility and are adopting incrementally. uv can replace just pip, or pip + virtualenv, or the whole stack.
Choose Rye if you want a single tool that manages everything and you are starting a new project. Rye's opinions reduce decision fatigue.
Runtime Testing Beyond the Python Codebase
uv and Rye handle the test environment reliably, but they can only test what your Python code does in isolation. For web applications, the deployed service behaves differently than the unit tests suggest — different environment variables, different network conditions, real database state. HelpMeTest runs AI-powered automated tests against your live endpoints on a schedule, catching the class of bugs that no unit test will find. The Pro plan at $100/month pairs well with a fast uv-based CI pipeline: uv keeps your unit tests under 60 seconds, HelpMeTest covers the rest.
Summary
uv and Rye solve the same problem — slow, unreproducible Python environments — with slightly different scopes. Both are dramatically faster than pip, both use lock files for reproducibility, and both integrate cleanly into GitHub Actions. For new projects, standardize on one and commit the lock file from the first commit. For existing projects, uv is a low-friction drop-in that immediately improves CI speed. Either way, fast installs and reproducible environments mean your test suite runs every time, on every commit, without excuses.