Skip to content

chore(sdk): Migrate Python dependency management to uv workspaces #12686

@hbelmiro

Description

@hbelmiro

TL;DR: Replace 7 setup.py files, 18 requirements.txt files, and 46+ scattered pip install commands with a uv workspace, a single uv.lock, and uv sync.

Chore description

Migrate the KFP monorepo's Python dependency management from scattered setup.py + requirements.in/txt files to a unified uv workspace with:

  • A root pyproject.toml defining workspace members and shared constraints
  • Per-package pyproject.toml files replacing setup.py
  • A single uv.lock lockfile for reproducible builds
  • CI workflows updated to use uv sync instead of scattered pip install commands

Why is this needed?

Current State Analysis

The repository has fragmented Python dependency management that causes multiple problems:

Dependency Files Inventory

File Type Count Locations
setup.py 7 See below
requirements.txt 18 Scattered across repo
requirements.in 6 SDK, kubernetes_platform, api, backend
MANIFEST.in 3 SDK, kubernetes_platform, api
pyproject.toml 1 Only components/PyTorch (out of scope)

Published PyPI Packages (In Scope)

Package Location Version Source Dependencies
kfp sdk/python/ kfp/version.py kfp-pipeline-spec, kfp-server-api, 10+ others
kfp-kubernetes kubernetes_platform/python/ kfp/kubernetes/__init__.py kfp, protobuf
kfp-pipeline-spec api/v2alpha1/python/ Hardcoded in setup.py protobuf
kfp-server-api backend/api/v2beta1/python_http_client/ Hardcoded in setup.py urllib3, six, certifi, python-dateutil

Out-of-Scope setup.py Files

  • components/google-cloud/setup.py — managed separately
  • components/PyTorch/pytorch-kfp-components/setup.py — managed separately
  • backend/api/v1beta1/python_http_client/setup.py — deprecated v1beta1 API

CI Workflow Fragmentation

The CI currently has 46+ pip install commands spread across workflows and actions:

Workflow/Action pip install commands
kfp-sdk-tests.yml 6 commands
kfp-sdk-unit-tests.yml 2 commands
kfp-sdk-client-tests.yml 6 commands
gcpc-modules-tests.yml 5 commands
publish-packages.yml 8 commands (2 per package)
sdk-yapf.yml 1 command
sdk-component-yaml.yml 3 commands
docs-freshness.yml 1 command
readthedocs-builds.yml 1 command
.github/actions/kfp-k8s/action.yml 6 commands
.github/actions/protobuf/action.yml 4 commands
.github/actions/test-and-report/action.yml 1 command
.github/actions/check-artifact-exists/action.yml 2 commands

Pain Points

  1. Dependabot chaos: Independent PRs for the same dependency across modules (e.g., protobuf updated in sdk/python and api/v2alpha1/python separately)
  2. Version drift risk: Comments like # protobuf version should be identical to the one in kfp-pipeline-spec indicate manual coordination is required
  3. CI environment inconsistency: Each workflow installs dependencies independently with no lockfile protection
  4. Maintenance burden: hack/update-all-requirements.sh exists but only covers 4 of 6 requirements.in files

Labels

/area sdk
/area backend
/area testing


Proposed Solution

Adopt uv workspaces (uv 0.9+) with a unified lockfile.

Target Structure

/pyproject.toml                                         # Workspace root (not published)
/uv.lock                                                # Single lockfile (committed)
/sdk/python/pyproject.toml                              # kfp package
/kubernetes_platform/python/pyproject.toml              # kfp-kubernetes package
/api/v2alpha1/python/pyproject.toml                     # kfp-pipeline-spec package
/backend/api/v2beta1/python_http_client/pyproject.toml  # kfp-server-api package

Root pyproject.toml Template

[project]
name = "kfp-workspace"
version = "0.0.0"
requires-python = ">=3.9"

[tool.uv.workspace]
members = [
  "sdk/python",
  "kubernetes_platform/python",
  "api/v2alpha1/python",
  "backend/api/v2beta1/python_http_client",
]

[tool.uv]
# Shared constraints applied during resolution for ALL workspace members
constraint-dependencies = [
  "protobuf>=6.31.1,<7.0",
  "kubernetes>=8.0.0,<31",
  "google-auth>=1.6.1,<3",
  "google-api-core>=1.31.5,<3.0.0dev,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0",
  "google-cloud-storage>=2.2.1,<4",
  "PyYAML>=5.3,<7",
  "urllib3<3.0.0",
]

[project.optional-dependencies]
# Linting tools (pinned to avoid new lint failures)
lint = [
  "docformatter==1.4",
  "isort==5.10.1",
  "pycln==2.1.1",
  "pylint==2.17.7",
  "yapf==0.43.0",
]

# Testing tools
test = [
  "absl-py",
  "docker",
  "nbformat",
  "pytest",
  "pytest-cov",
  "pytest-xdist",
]

# Development (lint + test + type checking)
dev = [
  "kfp-workspace[lint,test]",
  "mypy",
  "pre-commit",
  "types-protobuf",
  "types-PyYAML",
  "types-requests",
  "types-tabulate",
]

# Documentation building
docs = [
  "autodocsumm",
  "m2r2",
  "sphinx",
  "sphinx-click",
  "sphinx-immaterial",
  "sphinx-rtd-theme",
]

# CI-specific tools (includes lint, test, and docs)
ci = [
  "kfp-workspace[lint,test,docs]",
  "build",
  "junit2html",
  "requests",
  "ruamel.yaml",
  "twine",
]

Package pyproject.toml Template (sdk/python)

[project]
name = "kfp"
dynamic = ["version"]
description = "Kubeflow Pipelines SDK"
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.9"
authors = [{ name = "The Kubeflow Authors" }]
classifiers = [
  "Intended Audience :: Developers",
  "Intended Audience :: Education",
  "Intended Audience :: Science/Research",
  "License :: OSI Approved :: Apache Software License",
  "Programming Language :: Python :: 3",
  "Programming Language :: Python :: 3.9",
  "Programming Language :: Python :: 3.10",
  "Programming Language :: Python :: 3.11",
  "Programming Language :: Python :: 3.12",
  "Programming Language :: Python :: 3.13",
  "Topic :: Scientific/Engineering",
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
  "Topic :: Software Development",
  "Topic :: Software Development :: Libraries",
  "Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
  "click>=8.1.8",
  "click-option-group==0.5.7",
  "docstring-parser>=0.7.3,<1",
  # Pin google-api-core version for the bug fixing in 1.31.5
  # https://github.com/googleapis/python-api-core/releases/tag/v1.31.5
  "google-api-core>=1.31.5,<3.0.0dev,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0",
  "google-auth>=1.6.1,<3",
  # https://github.com/googleapis/python-storage/blob/main/CHANGELOG.md#221-2022-03-15
  "google-cloud-storage>=2.2.1,<4",
  # Update the upper version whenever a new major version of the
  # kfp-pipeline-spec package is released.
  # Update the lower version when kfp sdk depends on new apis/fields in
  # kfp-pipeline-spec
  "kfp-pipeline-spec>=2.15.0,<3",
  # Update the upper version whenever a new major version of the
  # kfp-server-api package is released.
  # Update the lower version when kfp sdk depends on new apis/fields in
  # kfp-server-api
  "kfp-server-api>=2.15.0,<3",
  "kubernetes>=8.0.0,<31",
  # protobuf version should be identical to the one in kfp-pipeline-spec
  "protobuf>=6.31.1,<7.0",
  "PyYAML>=5.3,<7",
  "requests-toolbelt>=0.8.0,<2",
  "tabulate>=0.8.6,<1",
  "urllib3<3.0.0",
]

[project.optional-dependencies]
all = ["kfp[kubernetes,notebooks]", "docker"]
kubernetes = ["kfp-kubernetes>=2.15.0,<3"]
notebooks = ["nbclient>=0.10,<1", "ipykernel>=6,<7", "jupyter_client>=7,<9"]

[project.scripts]
kfp = "kfp.cli.__main__:main"
dsl-compile = "kfp.cli.compile_:main"

[project.urls]
Documentation = "https://kubeflow-pipelines.readthedocs.io/en/stable/"
"Bug Tracker" = "https://github.com/kubeflow/pipelines/issues"
Source = "https://github.com/kubeflow/pipelines/tree/master/sdk"
Changelog = "https://github.com/kubeflow/pipelines/blob/master/sdk/RELEASE.md"

[tool.uv.sources]
kfp-pipeline-spec = { workspace = true }
kfp-server-api = { workspace = true }
kfp-kubernetes = { workspace = true }

[tool.hatch.version]
path = "kfp/version.py"

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

Implementation Tasks

Important: This migration should be done atomically in a single PR to avoid confusion about where contributors should update dependencies during a transition period.

Phase 1: Workspace Setup

  • Create root /pyproject.toml with workspace configuration
  • Define constraint-dependencies for shared deps (protobuf, kubernetes, google-auth, etc.)
  • Define optional-dependencies groups: lint, test, dev, ci, docs
  • Generate initial uv.lock file

Phase 2: Package Conversion

2.1 Convert kfp-pipeline-spec (api/v2alpha1/python/)

  • Create pyproject.toml with:
    • name = "kfp-pipeline-spec"
    • version = "2.15.2" (or use dynamic versioning)
    • dependencies = ["protobuf>=6.31.1,<7.0"]
    • Build system: hatchling
  • Verify proto generation still works (generate_proto.py)
  • Remove setup.py, requirements.in, requirements.txt, MANIFEST.in
  • Update api/Makefile targets (python, python-dev)

2.2 Convert kfp-server-api (backend/api/v2beta1/python_http_client/)

  • Create pyproject.toml with:
    • name = "kfp-server-api"
    • version = "2.15.2" (or use dynamic versioning)
    • dependencies = ["urllib3>=1.15", "six>=1.10", "certifi", "python-dateutil"]
    • Build system: hatchling
  • Note: This package is auto-generated from swagger; ensure regeneration works and templates do not overwrite pyproject.toml
  • Remove setup.py, requirements.txt

2.3 Convert kfp (sdk/python/)

  • Create pyproject.toml with:
    • name = "kfp"
    • Dynamic version from kfp/version.py
    • Full dependencies list (see template above)
    • [tool.uv.sources] for workspace references (kfp-pipeline-spec, kfp-server-api, kfp-kubernetes)
    • Entry points (kfp, dsl-compile)
    • Optional dependencies (all, kubernetes, notebooks)
  • Remove setup.py, requirements.in, requirements.txt, requirements-dev.txt, MANIFEST.in
  • Update sdk/Makefile target

2.4 Convert kfp-kubernetes (kubernetes_platform/python/)

  • Create pyproject.toml with:
    • name = "kfp-kubernetes"
    • Dynamic version from kfp/kubernetes/__init__.py
    • dependencies = ["protobuf>=6.31.1,<7.0", "kfp>=2.15.0,<3"]
    • [tool.uv.sources] for workspace reference to kfp
  • Verify proto generation still works (generate_proto.py)
  • Remove setup.py, requirements.in, requirements.txt, requirements-dev.txt, MANIFEST.in
  • Update kubernetes_platform/Makefile targets

Phase 3: CI Workflow Updates

3.1 Create setup-python Composite Action

  • Create .github/actions/setup-python/action.yml:

    name: "Setup Python with uv"
    description: "Install Python, uv, and CI dependencies"
    inputs:
      python-version:
        description: "Python version to use"
        required: true
    runs:
      using: "composite"
      steps:
        - name: Install uv
          uses: astral-sh/setup-uv@v7
    
        - name: Install CI dependencies
          run: uv sync --python ${{ inputs.python-version }} --extra ci
          shell: bash
  • Add lockfile sync check workflow (runs on PRs that touch **/pyproject.toml or uv.lock, and all pushes to master and release branches):

    - name: Check uv.lock is in sync
      run: uv lock --check

3.2 Update SDK Test Workflows

  • kfp-sdk-tests.yml:
    • Use .github/actions/setup-python action
    • Update test run: uv run ./test/presubmit-tests-sdk.sh
  • kfp-sdk-unit-tests.yml:
    • Use .github/actions/setup-python action
  • kfp-sdk-client-tests.yml:
    • Use .github/actions/setup-python action

3.3 Update Other Workflows

  • gcpc-modules-tests.yml:
    • Use .github/actions/setup-python action
  • sdk-yapf.yml:
    • Use .github/actions/setup-python action
  • sdk-component-yaml.yml:
    • Use .github/actions/setup-python action
  • docs-freshness.yml:
    • Use .github/actions/setup-python action
  • readthedocs-builds.yml:
    • Replace actions/setup-python@v6 with .github/actions/setup-python
    • Remove .github/actions/protobuf and .github/actions/kfp-k8s steps (handled by uv sync)
    • Remove pip install -r docs/sdk/requirements.txt (docs deps are in ci group)
    • Update sphinx-build to use uv run sphinx-build
    • Generate protos after sync: make -C api python && make -C kubernetes_platform python
  • Update .readthedocs.yml:
    • Add build.tools.uv: "latest"
    • Replace python.install with build.commands using uv sync --extra docs
    • Add proto generation commands before sphinx build
  • kfp-kubernetes-native-migration-tests.yaml:
    • Use .github/actions/setup-python action

3.4 Update Publish Workflow

  • publish-packages.yml:
    • Use .github/actions/setup-python action
    • Update build commands to use uv build --package <name>
    • Keep twine check for validation

3.5 Update Composite Actions

  • .github/actions/kfp-k8s/action.yml:
    • Use .github/actions/setup-python action
    • Update wheel building to use uv build
  • .github/actions/protobuf/action.yml:
    • Use .github/actions/setup-python action
    • Update proto generation flow
  • .github/actions/test-and-report/action.yml:
    • Use .github/actions/setup-python action
  • .github/actions/check-artifact-exists/action.yml:
    • Use .github/actions/setup-python action

3.6 Configure Dependabot

  • Create/update .github/dependabot.yml to watch uv.lock:

    version: 2
    updates:
      - package-ecosystem: "uv"
        directory: "/"
        schedule:
          interval: "daily"

    Note: The uv ecosystem watches pyproject.toml and uv.lock. With workspaces, it sees all workspace members' dependencies through the unified uv.lock — no need to configure each subdirectory separately.

Phase 4: Update Pre-commit to Use uv

  • Update .pre-commit-config.yaml to use local hooks with uv run:

    # Python linting (uses versions from uv.lock)
    - repo: local
      hooks:
        - id: pycln
          name: pycln
          entry: uv run pycln --all
          language: system
          types: [python]
        - id: isort
          name: isort
          entry: uv run isort --profile google
          language: system
          types: [python]
        - id: yapf
          name: yapf
          entry: uv run yapf -i
          language: system
          types: [python]
        - id: docformatter
          name: docformatter
          entry: uv run docformatter -i -r
          language: system
          types: [python]
          exclude: (sdk/python/kfp/compiler/compiler_test.py|kubernetes_platform/python/)
  • Remove old repo-based hooks for pycln, isort, yapf, docformatter

  • Keep non-Python hooks (pre-commit-hooks, flake8, golangci-lint, actionlint) as repo-based

  • Test that pre-commit run works on a sample changed file (running --all-files may surface pre-existing violations unrelated to this migration)

Phase 5: Documentation & Cleanup

  • Update sdk/CONTRIBUTING.md:
    • Replace pip install -r requirements-dev.txt with uv sync --extra dev
    • Replace pip install -e sdk/python with uv sync (auto-installs editable)
    • Update testing instructions to use uv run pytest
  • Update AGENTS.md:
    • Update "Local development setup" section
    • Update "Local testing" section
  • Remove old files:
    • sdk/python/requirements.in
    • sdk/python/requirements.txt
    • sdk/python/requirements-dev.txt
    • sdk/python/setup.py
    • sdk/python/MANIFEST.in
    • kubernetes_platform/python/requirements.in
    • kubernetes_platform/python/requirements.txt
    • kubernetes_platform/python/requirements-dev.txt
    • kubernetes_platform/python/setup.py
    • kubernetes_platform/python/MANIFEST.in
    • api/v2alpha1/python/requirements.in
    • api/v2alpha1/python/requirements.txt
    • api/v2alpha1/python/setup.py
    • api/v2alpha1/python/MANIFEST.in
    • backend/api/v2beta1/python_http_client/setup.py
    • backend/api/v2beta1/python_http_client/requirements.txt
  • Update or remove scripts that use pip-compile or setup.py:
    • hack/update-all-requirements.sh — remove (uv handles lockfile)
    • hack/update-requirements.sh — remove (uv handles lockfile)
    • sdk/python/pre-release-requirements-update.sh — remove (uv handles lockfile)
    • kubernetes_platform/python/pre-release-requirements-update.sh — remove (uv handles lockfile)
    • kubernetes_platform/python/release.sh — update to use uv build and uv publish
    • backend/update_requirements.sh — keep (backend services, out of scope)
    • backend/metadata_writer/update_requirements.sh — keep (backend services, out of scope)
    • backend/src/apiserver/visualization/update_requirements.sh — keep (backend services, out of scope)

Acceptance Criteria

Functional Requirements

  • AC1: All 4 packages (kfp, kfp-kubernetes, kfp-pipeline-spec, kfp-server-api) build successfully with uv build --package <name>
  • AC2: All 4 packages can be published to PyPI (verify with dry-run)
  • AC3: uv sync at repo root installs all workspace packages in editable mode
  • AC4: uv sync --extra ci installs CI dependencies (includes test)
  • AC5: uv sync --extra dev installs all development dependencies
  • AC6: Inter-package dependencies resolve correctly (e.g., kfp-kubernetes uses local kfp)
  • AC7: Proto generation works for kfp-pipeline-spec and kfp-kubernetes

CI Requirements

  • AC8: All existing CI workflows pass after migration
  • AC9: uv lock --check runs in CI to catch lockfile drift
  • AC10: No pip install commands remain in any CI workflow
  • AC11: Dependabot can create PRs for uv.lock updates

Documentation Requirements

  • AC12: sdk/CONTRIBUTING.md updated with uv commands
  • AC13: AGENTS.md updated with uv workflow
  • AC14: No references to removed files (requirements.in, setup.py, etc.)

Cleanup Requirements

  • AC15: All old setup.py files removed (except out-of-scope components)
  • AC16: All old requirements.in files removed (for in-scope packages)
  • AC17: All old requirements.txt files removed (for in-scope packages)
  • AC18: hack/update-all-requirements.sh removed
  • AC19: All MANIFEST.in files removed (for in-scope packages)
  • AC20: .pre-commit-config.yaml Python lint hooks use uv run (versions from uv.lock)

Implementation Notes for Code Agents

Key Constraints

  1. Atomic Migration: Do NOT create separate PRs for each package. The entire migration must be in a single PR to avoid contributor confusion.

  2. Version Preservation: Keep existing version numbers. Use dynamic = ["version"] with [tool.hatch.version] to read from existing version.py or __init__.py files.

  3. Dependency Bounds: The [project].dependencies in each package's pyproject.toml MUST match the bounds from the original requirements.in or setup.py. These are published to PyPI and affect end users.

  4. Workspace Sources: Use [tool.uv.sources] with workspace = true for inter-package dependencies during development. This does NOT affect published packages.

  5. Proto Files: The generated *_pb2.py files are NOT committed. Proto generation must still work after migration:

    • api/v2alpha1/python/generate_proto.py
    • kubernetes_platform/python/generate_proto.py
  6. Build Backend: Use hatchling as the build backend for consistency. It supports dynamic versioning and works well with uv.

  7. Workspace Root: Run uv commands from the repo root (or pass --project .) so the workspace and uv.lock are used. Running from subdirectories can create an unintended local lockfile.

  8. Publishing Secrets: Use GitHub Secrets for publish credentials (e.g., UV_PUBLISH_TOKEN), and avoid echoing tokens in logs.

Assumptions

  • uv 0.9+ is required for local development (CI uses astral-sh/setup-uv action).
  • Python 3.9+ is the minimum supported runtime across all in-scope packages.
  • The workspace root is not published to PyPI; only member packages are.

Testing the Migration Locally

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

# From repo root, sync all packages (editable mode)
uv sync

# Generate proto files (required before running tests/importing packages)
make -C api python
make -C kubernetes_platform python

# Verify packages are installed
uv pip list | grep kfp

# Run tests
uv run pytest sdk/python/kfp

# Build a specific package
uv build --package kfp

# Verify lockfile is in sync
uv lock --check

Common Pitfalls

  1. Don't add [build-system] to root pyproject.toml: The workspace root is not a publishable package.

  2. Don't forget [tool.uv.sources]: Without this, uv sync will try to install inter-package deps from PyPI instead of local source.

  3. Package data may be missing: When removing MANIFEST.in, verify any non-Python files (schemas, templates, etc.) are included via tool.hatch.build settings.


Related Issues/PRs


Love this idea? Give it a 👍.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions