uvとRuffで実現する高速で堅牢なPython開発環境の構築

に公開

はじめに

Pythonのパッケージ管理やフォーマッターの速度に不満を感じたことはありませんか。pipやpoetryは便利ですが、大規模プロジェクトでは依存関係の解決に時間がかかります。また、blackやflake8を使ったコード品質管理も、実行に数秒かかることがあります。

本記事では、Rust製の高速ツールであるuvとRuffを使って、モダンで効率的なPython開発環境を構築する方法を実際のプロジェクトを例に解説します。FastAPIとPostgreSQLを使った1000万件データ対応のAPIシステムを題材に、開発からデプロイまでの実践的な手法を紹介します。

対象読者

  • Python開発でパッケージ管理やフォーマット処理の遅さに悩んでいる方
  • モダンなPython開発環境を構築したい方
  • FastAPIやDocker環境でのベストプラクティスを知りたい方
  • コード品質管理を自動化したい方

プロジェクト概要

今回題材とするのは、1000万件のデータに対して1秒以内に応答する超高速APIシステムです。以下のような特徴があります。

  • バックエンド: FastAPI + PostgreSQL + SQLAlchemy
  • パッケージ管理: uv
  • コード品質管理: Ruff + mypy + pre-commit
  • テスト: pytest + pytest-asyncio
  • コンテナ: Docker + Docker Compose

プロジェクト構造は以下のようになっています。

backend/
├── src/
│   └── app/
│       ├── auth/              # 認証モジュール
│       ├── products/          # 商品モジュール
│       ├── settings/          # 設定モジュール
│       ├── database/          # データベース
│       └── main.py            # FastAPIアプリ
├── tests/
│   ├── unit/                  # 単体テスト
│   └── performance/           # パフォーマンステスト
├── scripts/                   # ユーティリティ
├── Dockerfile
├── compose.yml
└── pyproject.toml

詳細はGitHubで公開していますので、backendディレクトリの中をご確認ください。

Flutter側で商品一覧を操作した際の画面キャプチャーは以下になります。無限スクロールとフィルタリングでの遅延が発生していません。
画面イメージ

uvとは何か

uvは、Astral社が開発したRust製のPythonパッケージマネージャーです。従来のpipやpoetryと比較して、桁違いの高速性を実現しています。

uvの特徴

  • 速度の優位性

uvの最大の特徴は、その圧倒的な速度です。Rust製であるため、パッケージのダウンロードや依存関係の解決が非常に高速に行われます。例えば、pipで数分かかっていた依存関係のインストールが、uvでは数秒で完了することもあります。

  • PEP準拠の設計

uvは、Python Enhancement Proposalに準拠した設計になっています。pyproject.tomlを使った標準的なプロジェクト管理が可能で、既存のツールとの互換性も高くなっています。

  • 統合的な機能

パッケージ管理だけでなく、仮想環境の作成や管理、スクリプトの実行など、Python開発に必要な機能を統合的に提供します。これにより、複数のツールを使い分ける必要がなくなります。

uvのインストールと基本設定

インストール方法

uvのインストールは非常に簡単です。以下のコマンドを実行するだけで、システムにuvがインストールされます。

curl -LsSf https://astral.sh/uv/install.sh | sh

macOSやLinuxでは、インストール後にパスを通す必要があります。

export PATH="$HOME/.cargo/bin:$PATH"

この設定を永続化するには、.bashrcや.zshrcに追記します。

echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

インストールが完了したら、バージョンを確認して動作を確認します。

uv --version

プロジェクトの初期化

新しいプロジェクトを始める場合は、uv initコマンドを使用します。

uv init ultra-fast-api
cd ultra-fast-api

既存のプロジェクトに適用する場合は、pyproject.tomlが既に存在していれば、そのまま使用できます。今回のプロジェクトでは、以下のようなpyproject.tomlを使用しています。

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "ultra-fast-api"
version = "0.1.0"
description = "Ultra-fast API with FastAPI, PostgreSQL, and Flutter"
readme = "README.md"
requires-python = ">=3.12,<3.13"
license = {text = "MIT"}

dependencies = [
    "fastapi==0.115.0",
    "uvicorn[standard]==0.30.0",
    "pydantic==2.8.2",
    "sqlalchemy==2.0.36",
    "asyncpg==0.30.0",
    "alembic==1.14.0",
    "python-jose[cryptography]==3.3.0",
    "passlib[bcrypt]==1.7.4",
]

[dependency-groups]
dev = [
    "pytest==8.2.2",
    "pytest-asyncio==0.24.0",
    "pytest-cov==5.0.0",
    "ruff==0.6.9",
    "mypy==1.11.1",
]

このpyproject.tomlの構造について詳しく見ていきます。

  • build-system

ビルドシステムの設定です。hatchlingを使用することで、パッケージのビルドとパブリッシュが簡単になります。

  • project

プロジェクトのメタデータを定義します。nameやversionだけでなく、依存関係もここに記述します。

  • dependencies

本番環境で必要なパッケージを列挙します。バージョンを明示的に指定することで、再現性の高い環境を構築できます。

  • dependency-groups

開発環境専用のパッケージは、dependency-groupsのdevセクションに記述します。これにより、本番環境に不要なパッケージを分離できます。

依存関係のインストール

プロジェクトの依存関係をインストールするには、uv syncコマンドを使用します。

uv sync

このコマンドは、pyproject.tomlに記述された依存関係を読み込み、仮想環境を作成してパッケージをインストールします。初回実行時には.venvディレクトリが作成され、そこに仮想環境が構築されます。

開発用の依存関係も含めてインストールする場合は、デフォルトで含まれます。本番環境向けに開発用パッケージを除外したい場合は、以下のようにします。

uv sync --no-dev

仮想環境の管理

uvは仮想環境の管理も自動的に行います。明示的に仮想環境を作成したい場合は、以下のコマンドを使用します。

uv venv

仮想環境をアクティベートするには、通常のPython仮想環境と同じ方法を使います。

source .venv/bin/activate

ただし、uvを使ったコマンド実行では、仮想環境を明示的にアクティベートする必要がありません。uv runコマンドを使えば、自動的に仮想環境内でコマンドが実行されます。

uv run python script.py
uv run pytest
uv run uvicorn app.main:app

これにより、仮想環境の有効化と無効化を気にする必要がなくなり、開発体験が向上します。

パッケージの追加と削除

開発中に新しいパッケージが必要になった場合は、uv addコマンドを使用します。

uv add httpx

このコマンドは、パッケージをインストールするだけでなく、pyproject.tomlも自動的に更新します。開発用パッケージを追加する場合は、--devフラグを付けます。

uv add --dev pytest-mock

パッケージを削除する場合は、uv removeコマンドを使用します。

uv remove httpx

これもpyproject.tomlから自動的にエントリが削除されるため、手動でファイルを編集する必要がありません。

Ruffによるコード品質管理

Ruffは、Rust製の高速なPythonリンターおよびフォーマッターです。従来のflake8、pylint、isort、blackなどの機能を統合し、圧倒的な速度で実行できます。

Ruffの特徴

  • 統合的なツール

Ruffは、複数のリンティングルールとコードフォーマット機能を一つのツールで提供します。これまでflake8、isort、blackを個別に実行していた作業が、Ruffだけで完結します。

  • 高速な実行

Rust製であるため、大規模なコードベースでも数百ミリ秒でチェックが完了します。これにより、pre-commitフックやCIパイプラインでのストレスが大幅に軽減されます。

  • 豊富なルール

数百種類のリンティングルールがサポートされており、pyflakes、pycodestyle、mccabe、isort、pep8-namingなど、多くのツールのルールが利用可能です。

Ruffの設定

pyproject.tomlにRuffの設定を記述します。今回のプロジェクトでは、以下のような設定を使用しています。

[tool.ruff]
line-length = 120
target-version = "py312"
exclude = [
    ".git",
    ".venv",
    "__pycache__",
    ".pytest_cache",
    ".mypy_cache",
    "alembic",
]

[tool.ruff.lint]
select = [
    "E",      # pycodestyle errors
    "W",      # pycodestyle warnings
    "F",      # pyflakes
    "I",      # isort
    "C",      # flake8-comprehensions
    "B",      # flake8-bugbear
    "UP",     # pyupgrade
]
ignore = ["E501", "C901"]

[tool.ruff.lint.isort]
known-first-party = ["src"]
combine-as-imports = true

この設定について詳しく見ていきます。

  • line-length

1行の最大文字数を設定します。120文字に設定することで、現代的なワイドモニターでの可読性を保ちつつ、長すぎる行を防ぎます。

  • target-version

対象とするPythonバージョンを指定します。これにより、そのバージョンで利用可能な構文がチェックされます。

  • exclude

チェック対象から除外するディレクトリやファイルを指定します。仮想環境やキャッシュディレクトリは除外します。

  • select

有効にするリンティングルールを指定します。各文字が特定のルールセットを表しています。

  • E、W: PEP 8のスタイルエラーと警告

  • F: 未使用のインポートや変数などの論理エラー

  • I: インポート文の並び替え

  • C: リスト内包表記などの最適化提案

  • B: よくあるバグパターンの検出

  • UP: 新しいPython構文への自動アップグレード提案

  • ignore

特定のルールを無視します。E501は行の長さ制限ですが、line-lengthで管理するため無視します。C901は循環的複雑度ですが、一部の複雑な処理では意図的に高くなることがあるため無視します。

  • isort

インポート文の並び替え設定です。known-first-partyで自プロジェクトのパッケージを指定し、combine-as-importsで複数のfromインポートをまとめます。

Ruffの実行方法

コードのリンティングを実行するには、以下のコマンドを使用します。

uv run ruff check .

自動修正可能な問題を修正するには、--fixフラグを付けます。

uv run ruff check . --fix

コードフォーマットを実行するには、ruff formatコマンドを使用します。

uv run ruff format .

これにより、blackと同等のフォーマットがRustの速度で適用されます。

実践的なRuffの使い方

開発ワークフローでは、以下のような使い方が効果的です。

  • コードを書いた後の確認
uv run ruff check . --fix
uv run ruff format .
  • CIパイプラインでの厳密なチェック
uv run ruff check . --no-fix
uv run ruff format . --check

--no-fixフラグを付けることで、修正せずにエラーだけを報告します。--checkフラグを付けることで、フォーマットが必要なファイルがあればエラーを返します。

型チェックとmypy

Pythonは動的型付け言語ですが、型ヒントを使うことで静的型チェックが可能になります。mypyを使用することで、実行前に型に関するエラーを検出できます。

mypyの設定

pyproject.tomlにmypyの設定を記述します。

[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
ignore_missing_imports = true
  • python_version

対象とするPythonバージョンを指定します。

  • warn_return_any

関数がAny型を返す場合に警告を出します。これにより、型の明示性が向上します。

  • disallow_untyped_defs

型ヒントのない関数定義を禁止します。厳密な型チェックを行いたい場合はtrueに設定しますが、既存のコードベースでは段階的に導入するためにfalseにしておくことがあります。

  • ignore_missing_imports

型スタブがないサードパーティライブラリのインポートエラーを無視します。全てのライブラリが型情報を提供しているわけではないため、この設定が必要になります。

mypyの実行

型チェックを実行するには、以下のコマンドを使用します。

uv run mypy src/

エラーが検出された場合は、ファイルと行番号とともにエラーメッセージが表示されます。

pre-commitによる自動チェック

pre-commitは、Gitコミット前に自動的にチェックを実行するツールです。これにより、問題のあるコードがコミットされることを防ぎます。

pre-commitの設定

プロジェクトのルートに.pre-commit-config.yamlファイルを作成します。

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9
    hooks:
      - id: ruff-format
        description: "Ruff formatting"
        types_or: [python, pyi, jupyter]
      - id: ruff
        description: "Ruff linting"
        types_or: [python, pyi, jupyter]
        args: [--fix, --exit-non-zero-on-fix]

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.11.1
    hooks:
      - id: mypy
        description: "Run mypy"
        types: [python]
        additional_dependencies: ["types-all"]
        args: [--strict, --ignore-missing-imports]

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
        args: ["--maxkb=1000"]
      - id: check-merge-conflict

この設定により、コミット時に以下のチェックが自動実行されます。

  • Ruffによるコードフォーマット
  • Ruffによるリンティング
  • mypyによる型チェック
  • 末尾の空白削除
  • ファイル末尾の改行追加
  • YAMLファイルの構文チェック
  • 大きなファイルの検出
  • マージコンフリクトマーカーの検出

pre-commitのインストールと実行

pre-commitをインストールします。

uv add --dev pre-commit

Gitフックをインストールします。

uv run pre-commit install

これにより、git commitを実行するたびに自動的にチェックが実行されます。手動で全ファイルをチェックする場合は、以下のコマンドを使用します。

uv run pre-commit run --all-files

テスト環境の構築

テストは、コード品質を保証するための重要な要素です。pytestを使用した効果的なテスト環境を構築します。

pytestの設定

pyproject.tomlにpytestの設定を記述します。

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = "--strict-markers -v"
markers = [
    "slow: marks tests as slow",
    "integration: marks tests as integration",
]
  • testpaths

テストファイルの検索パスを指定します。

  • asyncio_mode

非同期テストの実行モードを指定します。autoに設定することで、async defで定義されたテスト関数が自動的に非同期として実行されます。

  • addopts

pytestに渡すデフォルトオプションを指定します。--strict-markersは未定義のマーカーをエラーにし、-vは詳細な出力を有効にします。

  • markers

カスタムマーカーを定義します。これにより、テストを分類して実行できます。

テストの実行

全てのテストを実行するには、以下のコマンドを使用します。

uv run pytest

特定のマーカーを持つテストのみを実行することもできます。

uv run pytest -m "not slow"

カバレッジ情報を取得するには、pytest-covを使用します。

uv run pytest --cov=src --cov-report=html

これにより、htmlcovディレクトリにHTMLレポートが生成されます。ブラウザで開いて、どの行がテストされているかを視覚的に確認できます。

Docker環境での統合

本番環境やCI/CD環境では、Dockerを使用してコンテナ化することが一般的です。uvをDockerで使用する方法を見ていきます。

マルチステージビルド

効率的なDockerイメージを作成するために、マルチステージビルドを使用します。

# ビルドステージ
FROM python:3.12-slim as builder

WORKDIR /app

# uvのインストール
RUN apt-get update && apt-get install -y curl && \
    curl -LsSf https://astral.sh/uv/install.sh | sh && \
    apt-get clean && rm -rf /var/lib/apt/lists/*
ENV PATH="/root/.local/bin:$PATH"

# プロジェクトファイルのコピー
COPY pyproject.toml uv.lock* README.md ./

# 依存関係のインストール
RUN uv sync --frozen --no-dev


# ランタイムステージ
FROM python:3.12-slim

WORKDIR /app

# uvとpostgresql-clientのインストール
RUN apt-get update && apt-get install -y curl postgresql-client && \
    curl -LsSf https://astral.sh/uv/install.sh | sh && \
    apt-get clean && rm -rf /var/lib/apt/lists/*
ENV PATH="/root/.local/bin:$PATH"

# ビルドステージから仮想環境をコピー
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"

# プロジェクトファイルのコピー
COPY pyproject.toml README.md ./
COPY alembic.ini ./
COPY src/ ./src/
COPY alembic/ ./alembic/
COPY scripts/ ./scripts/

# 非rootユーザーの作成
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app && \
    cp -r /root/.local /home/appuser/.local && \
    chown -R appuser:appuser /home/appuser/.local

USER appuser
ENV PATH="/home/appuser/.local/bin:$PATH"

# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
    CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1

# アプリケーションの起動
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

このDockerfileのポイントを解説します。

  • マルチステージビルド

builderステージで依存関係をインストールし、最終イメージには必要なファイルだけをコピーします。これにより、イメージサイズが大幅に削減されます。

  • uv sync --frozen

--frozenフラグを付けることで、uv.lockファイルに記録された正確なバージョンがインストールされます。これにより、開発環境と本番環境で完全に同じ依存関係が保証されます。

  • --no-dev

本番環境では開発用パッケージが不要なため、--no-devフラグを付けてインストールから除外します。

  • 非rootユーザー

セキュリティのため、アプリケーションは非rootユーザーで実行します。appuserを作成し、必要なファイルの所有権を変更します。

  • ヘルスチェック

コンテナのヘルスチェックを定義することで、アプリケーションが正常に動作しているかを監視できます。

Docker Composeでの開発環境

開発環境では、ソースコードの変更を即座に反映できるように、Docker Composeを使用します。

services:
  db:
    image: postgres:17-alpine
    container_name: ultra_fast_db
    environment:
      POSTGRES_USER: ${DB_USER:-postgres}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
      POSTGRES_DB: ${DB_NAME:-ultra_fast_db}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres}"]
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: ultra_fast_api
    environment:
      DATABASE_URL: postgresql+asyncpg://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@db:5432/${DB_NAME:-ultra_fast_db}
      JWT_SECRET_KEY: ${JWT_SECRET_KEY:-your-secret-key}
    ports:
      - "8000:8000"
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./src:/app/src
      - ./tests:/app/tests
    command: uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

volumes:
  postgres_data:
  • ヘルスチェック依存

apiサービスは、dbサービスがhealthyになるまで待機します。これにより、データベースが準備できる前にアプリケーションが起動してエラーになることを防ぎます。

  • ボリュームマウント

開発環境では、ソースコードをボリュームマウントすることで、ホスト側でコードを編集するとコンテナ内に即座に反映されます。

  • --reload**

uvicornの--reloadフラグを付けることで、ファイルが変更されると自動的にアプリケーションがリロードされます。

コンテナ内でのコマンド実行

コンテナ内でuvコマンドを実行するには、docker compose execを使用します。

# テストの実行
docker compose exec api uv run pytest

# マイグレーションの実行
docker compose exec api uv run alembic upgrade head

# コード品質チェック
docker compose exec api uv run ruff check .
docker compose exec api uv run mypy src/

実践的なワークフロー

ここまでで紹介したツールを組み合わせた、実践的な開発ワークフローを紹介します。

新機能の開発

新しい機能を開発する際の典型的な流れは以下のようになります。

  1. ブランチの作成

    git checkout -b feature/new-endpoint
    
  2. コードの実装

    必要に応じてパッケージを追加します。

    uv add httpx
    

    機能を実装します。この際、型ヒント(Type Hints)を必ず付与します。型ヒントは以下のようなメリットをもたらします。

    • 実行前にバグを発見: mypyが型の不整合を検出し、実行前にエラーを発見できます
    • IDEの補完強化: エディタが正確な型情報を認識し、コード補完が向上します
    • ドキュメント効果: 関数のシグネチャを見るだけで、引数と戻り値の型が明確になります
    • リファクタリング支援: 型変更時に影響範囲を自動的に検出できます
    # 型ヒントの例
    from uuid import UUID
    from sqlalchemy.ext.asyncio import AsyncSession
    
    async def get_user_by_id(
        user_id: UUID,
        db: AsyncSession
    ) -> User | None:
        """IDでユーザーを取得する
        
        Args:
            user_id: ユーザーID
            db: データベースセッション
            
        Returns:
            ユーザーオブジェクト、存在しない場合はNone
        """
        result = await db.execute(
            select(User).where(User.id == user_id)
        )
        return result.scalar_one_or_none()
    

    このように型ヒントを付けることで、後のmypyチェックで型エラーを自動検出でき、バグの混入を防ぎます。

  3. テストの作成(TDDサイクル)

    TDD(Test-Driven Development)の原則に従い、Red-Green-Refactorサイクルで開発を進めます。このサイクルは品質の高いコードを効率的に書くための実証済みの手法です。

    Red(失敗するテストを書く)

    まず、新機能の期待動作を記述したテストを書きます。この時点では実装がないため、テストは必ず失敗します。

    # tests/unit/test_user_service.py
    import pytest
    from app.services.user_service import UserService
    
    class TestUserService:
        @pytest.mark.asyncio
        async def test_update_user_profile(self, db_session):
            """ユーザープロフィール更新のテスト"""
            # Arrange
            service = UserService(db_session)
            user_id = UUID("...")
            new_name = "Updated Name"
            
            # Act
            result = await service.update_profile(user_id, new_name)
            
            # Assert
            assert result.name == new_name
            assert result.updated_at > result.created_at
    

    テストを実行して、期待通り失敗することを確認します。

    uv run pytest tests/unit/test_user_service.py -v
    # FAILED - まだ実装がないため失敗する(これが正しい状態)
    

    Green(テストをパスさせる最小限の実装)

    テストをパスさせるための最小限のコードを実装します。まずは動作させることを優先し、最適化は後回しにします。

    # src/app/services/user_service.py
    from uuid import UUID
    from datetime import UTC, datetime
    
    class UserService:
        async def update_profile(
            self,
            user_id: UUID,
            name: str
        ) -> User:
            """ユーザープロフィールを更新"""
            user = await self.get_user_by_id(user_id)
            user.name = name
            user.updated_at = datetime.now(UTC)
            await self.db.commit()
            return user
    

    再度テストを実行して、成功することを確認します。

    uv run pytest tests/unit/test_user_service.py -v
    # PASSED - 実装が完了してテストが成功
    

    Refactor(リファクタリング)

    テストがパスした後、コードの品質を向上させます。重複の除去、命名の改善、パフォーマンスの最適化などを行います。テストがあるため、安心してリファクタリングできます。

    # リファクタリング後
    class UserService:
        async def update_profile(
            self,
            user_id: UUID,
            name: str
        ) -> User:
            """ユーザープロフィールを更新
            
            Args:
                user_id: 更新対象のユーザーID
                name: 新しい名前
                
            Returns:
                更新されたユーザーオブジェクト
                
            Raises:
                NotFoundError: ユーザーが存在しない場合
            """
            user = await self._get_user_or_raise(user_id)
            self._update_user_fields(user, name=name)
            await self._save_user(user)
            return user
    

    リファクタリング後も必ずテストを実行し、機能が壊れていないことを確認します。

    uv run pytest tests/unit/test_user_service.py -v
    # PASSED - リファクタリング後も動作することを確認
    

    このRed-Green-Refactorサイクルを繰り返すことで、テストカバレッジの高い堅牢なコードが完成します。

  4. コード品質チェック

    # フォーマットとリンティング
    uv run ruff format .
    uv run ruff check . --fix
    
    # 型チェック
    uv run mypy src/
    
    # 全テストの実行
    uv run pytest
    
  5. pre-commitでの最終チェック

    uv run pre-commit run --all-files
    
  6. コミット

    pre-commitがインストールされていれば、git commitを実行すると自動的にチェックが走ります。

    git add .
    git commit -m "feat: add new endpoint for user management"
    

依存関係の更新

パッケージを最新バージョンに更新する場合は、以下の手順を踏みます。

# ロックファイルの更新
uv lock --upgrade

# 依存関係の再同期
uv sync

# テストの実行
uv run pytest

# 問題なければコミット
git add uv.lock pyproject.toml
git commit -m "chore: update dependencies"

パフォーマンステスト

大規模データを扱うAPIでは、パフォーマンステストが重要です。今回のプロジェクトでは、1000万件のデータに対するテストを実施しています。

# パフォーマンステスト専用スクリプトの実行
uv run python scripts/run_performance_tests.py

# またはpytestで特定のマーカーを実行
uv run pytest tests/performance/ -v -m slow

パフォーマンステストは実行時間が長いため、slowマーカーを付けて通常のテストと分離しています。

CI/CDへの統合

GitHub ActionsやGitLab CI/CDでuvを使用する方法を紹介します。

GitHub Actionsの例

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:17-alpine
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test_db
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      
      - name: Install uv
        uses: astral-sh/setup-uv@v1
      
      - name: Install dependencies
        run: |
          cd backend
          uv sync
      
      - name: Run linting
        run: |
          cd backend
          uv run ruff check .
          uv run ruff format . --check
      
      - name: Run type checking
        run: |
          cd backend
          uv run mypy src/
      
      - name: Run tests
        env:
          DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/test_db
        run: |
          cd backend
          uv run pytest --cov=src --cov-report=xml
      
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          file: ./backend/coverage.xml

このワークフローのポイントは以下の通りです。

  • astral-sh/setup-uv

公式のuvセットアップアクションを使用することで、簡単にuvをインストールできます。

  • PostgreSQLサービス

GitHub Actionsのサービスコンテナを使用して、PostgreSQLを起動します。これにより、実際のデータベースを使った統合テストが可能になります。

  • キャッシュ

uvは内部で依存関係をキャッシュするため、2回目以降の実行が高速になります。

GitLab CI/CDの例

image: python:3.12-slim

variables:
  DATABASE_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/test_db

services:
  - postgres:17-alpine

before_script:
  - apt-get update && apt-get install -y curl
  - curl -LsSf https://astral.sh/uv/install.sh | sh
  - export PATH="/root/.local/bin:$PATH"
  - cd backend
  - uv sync

stages:
  - lint
  - test
  - build

lint:
  stage: lint
  script:
    - uv run ruff check .
    - uv run ruff format . --check
    - uv run mypy src/

test:
  stage: test
  script:
    - uv run pytest --cov=src --cov-report=term --cov-report=xml
  coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

パフォーマンスの比較

このプロジェクトで実際に計測した、uvやRuffのパフォーマンスを紹介します。

パッケージ管理速度(84パッケージ、既にキャッシュあり)

実測値は以下の通りです。

  • uv sync
time uv sync
# 実測: 0.109秒(21ms解決 + 19ms監査)

uvは依存関係の解決とインストールが極めて高速です。特にキャッシュが有効な場合、ほぼ瞬時に完了します。

コードチェック速度(38ファイル、27ソースファイル)

プロジェクト全体に対するチェックの実測値です。

  • Ruff チェック
time uv run ruff check .
# 実測: 0.068秒
  • Ruff フォーマット
time uv run ruff format . --check
# 実測: 0.051秒
  • Ruff 合計(チェック + フォーマット)
time (uv run ruff check . && uv run ruff format . --check)
# 実測: 0.101秒
  • mypy 型チェック
time uv run mypy src/
# 実測: 0.689秒(27ファイル)
  • pytest テスト実行
time uv run pytest
# 実測: 6.575秒(50テスト)

速度の利点

これらの高速なツールを使用することで、以下のメリットが得られます。

  • 開発サイクルの高速化: コード変更後のチェックが1秒以内に完了するため、頻繁にチェックできます
  • CI/CDの短縮: パイプライン全体の実行時間が大幅に削減されます
  • pre-commitの実用性: コミット時のチェックが速いため、開発フローを妨げません
  • フィードバックループの短縮: 問題を即座に発見し、修正できます

従来のツール(flake8、black、isortの組み合わせ)と比較すると、Ruffは10倍以上高速です。

トラブルシューティング

開発中によくある問題と解決方法を紹介します。

uvのキャッシュ問題

まれに、uvのキャッシュが原因でパッケージのインストールに失敗することがあります。その場合は、キャッシュをクリアします。

uv cache clean
uv sync

特定のパッケージだけキャッシュを削除する場合は、以下のようにします。

uv cache clean <パッケージ名>

Ruffのルール調整

プロジェクトの状況によっては、特定のRuffルールが厳しすぎることがあります。その場合は、pyproject.tomlのignoreセクションにルールを追加します。

[tool.ruff.lint]
ignore = ["E501", "C901", "B008"]

特定のファイルや行だけルールを無視したい場合は、コメントを使用します。

# ruff: noqa: E501
long_line = "この行は非常に長いですが、特別な理由があるため許可します"

result = some_function()  # noqa: B008

Docker環境での権限問題

Dockerコンテナ内でuvを実行する際に、権限の問題が発生することがあります。これは、Dockerfileで適切にユーザー権限を設定することで解決します。

RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

依存関係の競合

複数のパッケージが同じ依存関係の異なるバージョンを要求する場合、競合が発生します。uvは自動的に解決を試みますが、解決できない場合はエラーメッセージが表示されます。その場合は、pyproject.tomlでバージョン制約を調整します。

dependencies = [
    "package-a>=1.0,<2.0",
    "package-b>=2.0",
]

まとめ

本記事では、uvとRuffを使った高速で堅牢なPython開発環境の構築方法を、実際のプロジェクトを例に解説しました。

主なポイント

  • 速度の劇的な向上

uvとRuffを導入することで、パッケージインストールやコード品質チェックの時間が大幅に短縮されます。これにより、開発者はコードの品質管理に時間を取られることなく、本質的な開発作業に集中できます。

  • 統合的なツールチェーン

複数のツールを組み合わせていた従来の方法から、uvとRuffという2つの主要ツールに集約することで、設定ファイルの管理が簡単になり、ツール間の競合も減少します。

  • 再現性の確保

pyproject.tomlとuv.lockを使用することで、開発環境、CI/CD環境、本番環境で完全に同じ依存関係を保証できます。これにより、環境差異によるバグが減少します。

  • Docker環境との親和性

uvのマルチステージビルド対応により、効率的なDockerイメージを作成できます。イメージサイズの削減とビルド時間の短縮を両立できます。

今後の展望

uvとRuffは活発に開発が続けられており、今後もさらなる機能追加やパフォーマンス向上が期待されます。Pythonエコシステムにおいて、Rust製ツールの採用は今後も進んでいくと考えられます。

参考リンク

この記事が、皆さんのPython開発環境の改善に役立てば幸いです。

Discussion