uv and Rye Python Toolchain Testing in CI: A Complete Guide

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 | sh

Or on macOS with Homebrew:

brew install uv

Project Setup with uv

Initialize a new project:

uv init my-project
cd my-project

This creates a pyproject.toml with a minimal project structure. Add dependencies:

uv add pytest pytest-cov httpx
uv add --dev ruff mypy

uv 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=xml

uv 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.xml

Key details:

  • setup-uv handles installation and caching
  • enable-cache: true caches the uv package cache between runs
  • uv sync --frozen installs exactly what is in uv.lock without updating it — critical for reproducibility
  • uv python install 3.12 pins 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 | bash

Project Setup with Rye

rye init my-project
cd my-project
rye add pytest pytest-cov
rye add --dev ruff mypy

Rye 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 environment

Like 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=src

Running 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/ -v

For 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 == 200

In 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.txt

For 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.

Read more