Skip to content

[FEATURE] Migrate project to use uv with PEP 735 Dependency Groups #447

@darixsamani

Description

@darixsamani

Problem

The project currently uses traditional Python dependency management tools (pip, requirements.txt). This leads to:

  • Slower dependency resolution and installation times
  • Inconsistent lock file formats
  • Complex virtual environment management
  • Slower CI/CD pipeline execution
  • Tool lock-in: Using tool-specific configurations (like Poetry's [tool.poetry]) makes it difficult to switch tools in the future

Modern Python projects benefit from faster, more reliable tooling. However, as discussed in #321, we should avoid vendor lock-in by using PEP 735 dependency groups - a Python standard that any compliant tool can use, not just one specific tool.

Proposed Solution

Migrate the FastAPI RESTful project to use uv as the primary dependency management tool, while using PEP 735 dependency groups for organizing dependencies. This approach provides:

  • Speed up dependency installation (10-100x faster than pip)
  • Provide a unified tool for virtual environment and dependency management
  • Generate a reliable uv.lock file for reproducible builds
  • Improve developer experience with faster feedback loops
  • Optimize CI/CD pipeline performance
  • No vendor lock-in: PEP 735 is a Python standard that works with any compliant tool (uv today, pip in the future, and potentially Poetry when they add support)

This combines the performance benefits of uv with the flexibility of standards-based configuration.

Suggested Approach

1. Install uv locally for testing

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

2. Create PEP 735-compliant pyproject.toml

Instead of using uv-specific or Poetry-specific configuration, use the standard PEP 735 format:

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

[project]
name = "fastapi-restful-sample"
version = "1.0.0"
description = "FastAPI RESTful API sample with SQLAlchemy"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.100.0",
    "uvicorn[standard]>=0.23.0",
    "sqlalchemy>=2.0.0",
    "pydantic>=2.0.0",
    "pydantic-settings>=2.0.0",
]

[dependency-groups]
test = [
    "pytest>=7.4.0",
    "pytest-cov>=4.1.0",
    "pytest-asyncio>=0.21.0",
    "httpx>=0.24.0",
]
lint = [
    "ruff>=0.1.0",
    "mypy>=1.5.0",
]
dev = [
    {include-group = "test"},
    {include-group = "lint"},
]

Key difference from original proposal: We're using [dependency-groups] (PEP 735 standard) instead of tool-specific sections. This means the same pyproject.toml will work with uv today and pip tomorrow, without modifications.

3. Initialize virtual environment and install dependencies

# Create virtual environment
uv venv

# Activate it
source .venv/bin/activate  # Linux/macOS
# or
.venv\Scripts\activate  # Windows

# Install project dependencies
uv pip install -e .

# Install with development dependencies
uv pip install -e . --group dev

# Or install specific dependency groups
uv pip install -e . --group test
uv pip install -e . --group lint

4. Key files to modify

pyproject.toml

  • Add [project] section with core dependencies
  • Add [dependency-groups] for test, lint, and dev groups (PEP 735 format)
  • Add [build-system] configuration

.gitignore

# Virtual environment
.venv/
venv/

# uv
uv.lock

# Python
__pycache__/
*.py[cod]
*$py.class

README.md

Update setup instructions:

## Setup

### Prerequisites
- Python 3.11+
- [uv](https://docs.astral.sh/uv/) (recommended) or pip

### Installation

1. Install uv:
   ```bash
   curl -LsSf https://astral.sh/uv/install.sh | sh
  1. Create virtual environment and install dependencies:

    uv venv
    source .venv/bin/activate  # Linux/macOS
    uv pip install -e . --group dev
  2. Run the application:

    uv run uvicorn main:app --reload

Running Tests

uv run pytest

Code Quality

uv run ruff check .
uv run mypy .

#### `.github/workflows/python-app.yml`
Update the existing GitHub Actions workflow with the following changes:

```yaml
# Building and testing Python
# https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python CI

permissions:
  contents: read

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/[email protected]

      - name: Lint commit messages
        uses: wagoid/[email protected]

      - name: Set up uv
        uses: astral-sh/setup-uv@v4
        with:
          version: "latest"

      - name: Set up Python
        uses: actions/[email protected]
        with:
          python-version-file: '.python-version'

      - name: Install dependencies
        run: |
          uv venv
          uv pip install -e . --group lint

      - name: Lint with Flake8
        run: |
          uv run flake8 .

      - name: Check code formatting with Black
        run: |
          uv run black --check .

  test:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/[email protected]

      - name: Set up uv
        uses: astral-sh/setup-uv@v4
        with:
          version: "latest"

      - name: Set up Python
        uses: actions/[email protected]
        with:
          python-version-file: '.python-version'

      - name: Install dependencies
        run: |
          uv venv
          uv pip install -e . --group test

      - name: Run tests with pytest
        run: |
          uv run pytest -v

      - name: Generate coverage report
        run: |
          uv run pytest --cov=./ --cov-report=xml --cov-report=term

      - name: Upload coverage report artifact
        uses: actions/[email protected]
        with:
          name: coverage.xml
          path: ./coverage.xml

  coverage:
    needs: test
    runs-on: ubuntu-latest
    # Only run coverage for PRs from the same repository (not forks)
    # This ensures secrets are available for Codecov and Codacy
    if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
    strategy:
      matrix:
        service: [codecov, codacy]
    steps:
      - name: Checkout repository
        uses: actions/[email protected]

      - name: Download coverage report artifact
        uses: actions/[email protected]
        with:
          name: coverage.xml

      - name: Upload coverage report to ${{ matrix.service }}
        if: ${{ matrix.service == 'codecov' }}
        uses: codecov/[email protected]
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: coverage.xml

      - name: Upload coverage report to ${{ matrix.service }}
        if: ${{ matrix.service == 'codacy' }}
        uses: codacy/[email protected]
        with:
          project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
          coverage-reports: coverage.xml

  container:
    needs: coverage
    if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
    runs-on: ubuntu-latest

    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/[email protected]

      - name: Log in to GitHub Container Registry
        uses: docker/[email protected]
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Set up Docker Buildx
        uses: docker/[email protected]

      - name: Build and push Docker image to GitHub Container Registry
        uses: docker/[email protected]
        with:
          context: .
          push: true
          platforms: linux/amd64
          provenance: false
          cache-from: type=gha
          cache-to: type=gha,mode=max
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:sha-${{ github.sha }}

Key changes:

  • Removed cache: 'pip' from setup-python (uv handles caching)
  • Added astral-sh/setup-uv@v4 action before Python setup in both lint and test jobs
  • Changed pip install -r requirements-lint.txt to uv venv && uv pip install -e . --group lint
  • Changed pip install -r requirements-test.txt to uv venv && uv pip install -e . --group test
  • Prefixed flake8, black, and pytest commands with uv run
  • Kept all existing workflow structure, job dependencies, and artifact handling unchanged

Files to remove

  • requirements.txt
  • requirements-lint.txt
  • requirements-test.txt

5. Common commands

# Environment management
uv venv                          # Create virtual environment
uv venv --python 3.12           # Create with specific Python version

# Dependency management
uv pip install -e .              # Install project dependencies
uv pip install -e . --group dev  # Install with dev dependencies
uv pip install -e . --group test # Install with test dependencies only
uv pip list                      # List installed packages

# Running commands
uv run uvicorn main:app --reload # Run application
uv run pytest                    # Run tests
uv run ruff check .              # Run linter
uv run mypy .                    # Run type checker

Acceptance Criteria

  • uv is successfully installed and configured for the project
  • pyproject.toml uses PEP 735 [dependency-groups] format (not tool-specific configuration)
  • All project dependencies are defined in [project.dependencies]
  • Development dependencies are organized in appropriate groups: test, lint, dev
  • Virtual environment can be created with uv venv and activated
  • Dependencies can be installed with uv pip install -e . --group dev
  • Application runs successfully using uv run uvicorn main:app --reload
  • All tests pass when executed via uv run pytest
  • Linting works with uv run ruff check . and uv run mypy .
  • CI/CD pipeline is updated to use uv and passes successfully
  • uv.lock file is generated and committed to repository
  • README.md includes clear instructions for setting up the project with uv
  • Old dependency management files are removed (requirements*.txt)
  • Installation time is measurably faster than previous setup (document the improvement)
  • Configuration is tool-agnostic and will work with pip when it adds PEP 735 support

References


Note on approach: This implementation differs from a pure uv migration by using PEP 735 dependency groups instead of uv-specific configuration. This gives us uv's performance benefits today while keeping our configuration standards-based and tool-agnostic for the future. When pip adds PEP 735 support, we won't need to change our pyproject.toml at all.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestpythonPull requests that update Python code

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions