# Migrating a Python project to Chainguard Libraries

URL: https://chainguard-docs-preview-git-fix-learning-lab-jsonld.chainguard.app/chainguard/libraries/python/migration.md
Last Modified: October 13, 2026
Tags: Chainguard Libraries, Python

How to migrate an existing Python project to pull dependencies from Chainguard Libraries


Chainguard Libraries for Python provides a curated index of PyPI packages rebuilt from source and verified against Chainguard's build provenance. Because Chainguard Libraries uses the standard pip simple-index protocol, switching an existing project requires only an index configuration change — no changes to your application code or dependency declarations.

This guide walks through migrating an existing Python project to Chainguard Libraries, covering the two most common setups:

* **Direct access**: Your build tool connects directly to `libraries.cgr.dev`. This option is faster for initial evaluation and smaller-scale setups.  
* **Repository manager**: Your build tool connects to a repository manager (such as JFrog Artifactory or Sonatype Nexus), which proxies requests to Chainguard Libraries. This option is recommended for teams and organizations.

To follow along with a ready-made project instead of your own, use the [Chainguard Libraries for Python demo repository](https://github.com/chainguard-demo/chainguard-libraries-python). It provides example projects for pip, uv, and Poetry, each with a `demo.sh` script that configures access and installs sample packages.

For a reference of the configuration options for each supported build tool, check out [Configure Python build tools](/chainguard/libraries/python/build-configuration/).

## Prepare for migration

A Python migration is more than changing an index URL; the application code usually stays the same, but dependency resolution can change because Python packages are distributed as different artifacts for different operating systems, Python versions, CPU architectures, and C libraries.

Plan for the following possible friction patterns:

* The Chainguard catalog may not have every package, version, or wheel variant in an existing lockfile.
    * Learn more under [Troubleshooting: Handling coverage gaps](#handling-coverage-gaps).
* A dependency may be available for Linux but not for macOS, Windows, Alpine, or a particular Python/architecture combination.
* A lockfile generated against PyPI contains hashes and artifact URLs that do not match Chainguard-built artifacts.
* CI needs an authentication model that is safe for unattended builds and compatible with the package manager and repository topology.

### Pre-migration readiness check

Before changing the index, record the dependency and environment matrix that the migration must support:

* Python versions used in development, CI, and production
* Operating systems and CPU architectures used by developers, CI runners, build images, and deployment jobs
* The package manager and lockfile format used by each job: `pip`, `uv`, Poetry, or another supported tool
* Direct dependencies and improtant transitive dependencies that include native code, GPU support, platform markers, or compiled extensions
* Whether the project uses hash-pinned requirements or lockfiles
* Where package caches exist: local machines, CI caches, repository managers, Docker layers, and base images

Run the existing build successfully before making changes. Keep the baseline dependency versions and record the commands used to install, test, and package the project. This gives your team a comparison point when a migration failure is caused by source selection, an artifact mismatch, or an unrelated dependency change.

## Prerequisites

Before you begin, you need:

* An existing, working Python project with a `requirements.txt`, `poetry.lock`, `uv.lock`, `pdm.lock`, `Pipfile.lock`, or `pylock.toml`
* [chainctl installed and authenticated](/platform/chainctl-usage/how-to-install-chainctl/)
* An [entitlement to Chainguard Libraries](/platform/chainctl/chainctl-docs/chainctl_libraries_entitlements_create/) for Python.

If you do not have an entitlement to Chainguard Libraries for Python yet, run the following command to create an entitlement and enable upstream fallback:

```shell
chainctl libraries entitlements create --ecosystems=PYTHON --policy=CHAINGUARD_AND_UPSTREAM
```

It can take up to 30 minutes for fallback policy changes to take effect.

### Authentication prerequisites

For authentication, you need a pull token or the Python keyring provider.



**Pull token**



If you plan to use a repository manager, or a non-interactive environment such as CI/CD, you will need a pull token. You must have the `owner` role or have the `libraries.python.pull_token_creator` permission to create one.

```shell
chainctl auth pull-token create --repository=python --name=my-python-token --ttl=720h
```

To export environment variables directly:

```shell
eval $(chainctl auth pull-token --output env --repository=python --name=my-python-token)
```

Learn more about creating and managing pull tokens in the [Libraries access documentation](/chainguard/libraries/introduction/access/#creating-pull-tokens-for-libraries).




**Python keyring**



The keyring leverages `chainctl` to fetch temporary credentials whenever your environment requests packages from Chainguard. Supported environments include local development and CI/CD platforms that can use assumable identities. The keyring provider requires pip 23.1 or later.

Learn how to install this package in the [Chainguard Libraries access documentation](/chainguard/libraries/introduction/access/#python-keyring-provider).

Note that `.netrc` takes precedence over the keyring. Verify that you do not have an existing registry entry in `.netrc` for Chainguard Libraries:

```shell
grep -A2 "libraries.cgr.dev" ~/.netrc
```

Remove or correct any stale entry before proceeding.







> **Note: Do not commit credentials to version control.** Store tokens as environment variables or CI secrets, not as literal values in `pip.conf`, `.netrc`, or any file that might be checked into a repository.

## Step 1: Configure authentication and registry

How you configure the index depends on your environment. Follow the instructions that match your setup.

The `https://libraries.cgr.dev/python/` endpoint is also the [Chainguard Repository](/chainguard/chainguard-repository/overview/) endpoint for Python. By default, it serves only Chainguard-built artifacts. When upstream fallback is enabled for your organization, the same endpoint can also serve requested versions from PyPI under Chainguard security controls.

### Direct access

#### Configure authentication

First, configure authentication using a pull token or the Python keyring provider.

Setting pull token credentials in `.netrc` is a common approach for individual work stations. Setting authentication in `pip.conf` is preferred for CI/CD, where you need credentials isolated per project or per pipeline rather than a single shared home-directory file.



**Pull token in .netrc**



> **Note**: `.netrc` only supports one set of credentials per hostname. Since all Chainguard Libraries are served from `libraries.cgr.dev`, configuring `.netrc` for Python will override credentials for any other ecosystem.

First, check for an existing registry entry in `.netrc`:

```shell
grep -A2 "libraries.cgr.dev" ~/.netrc
```

Remove or correct any stale entry before proceeding.

Then, configure your [pull token credentials](#authentication-prerequisites) in `.netrc`:

```shell
cat >> ~/.netrc << EOF
machine libraries.cgr.dev
login ${CHAINGUARD_PYTHON_IDENTITY_ID}
password ${CHAINGUARD_PYTHON_TOKEN}
EOF
chmod 600 ~/.netrc
```




**Pull token in pip.conf**



Unlike `.netrc`, `pip.conf` can be scoped per project or per pipeline rather than shared globally across a whole workstation.

First, check whether an index or credentials are already configured
elsewhere, since a global `pip.conf` or environment variable can silently
override a project-scoped one:

```shell
pip config list -v
env | grep -i pip
```

Then create a project-scoped config file rather than editing the global
`~/.pip/pip.conf`. This example sets the remediated repository first, with
the standard repository as a fallback, and embeds [pull token credentials](#authentication-prerequisites) directly in
each URL:

```shell
mkdir -p .pip
cat > .pip/pip.conf << EOF
[global]
index-url = https://${CHAINGUARD_PYTHON_IDENTITY_ID}:${CHAINGUARD_PYTHON_TOKEN}@libraries.cgr.dev/python-remediated/simple/
extra-index-url = https://${CHAINGUARD_PYTHON_IDENTITY_ID}:${CHAINGUARD_PYTHON_TOKEN}@libraries.cgr.dev/python/simple/
EOF
```

Point pip at it with the `PIP_CONFIG_FILE` environment variable, scoped to
your current shell or CI job:

```shell
export PIP_CONFIG_FILE="$(pwd)/.pip/pip.conf"
```

This method sets both the index and its credentials in one step.

> **Note**: Do not commit credentials to version control. Generate `.pip/pip.conf`
> at build time from CI secrets, as shown in this section, rather than committing a
> version with a literal token embedded in the URL. Add `.pip/` to
> `.gitignore` if you create it locally with real credentials for testing.




**Python keyring**



The `keyrings-chainguard-libraries` package supplies short-lived credentials automatically via `chainctl`, avoiding static tokens on your local workstation.

Follow the [instructions to install the keyring package](/chainguard/libraries/introduction/access/#python-keyring-provider).

> **Note**: After switching to Chainguard, you can [reinstall the keyring package](/chainguard/libraries/introduction/access/#python-keyring-provider) to use the Chainguard-built version.







#### Point your build tool at Chainguard

Next, point your build tool at the Chainguard index.

Note that Chainguard publishes both standard and [remediated Python indexes](/chainguard/libraries/policies-and-security/cve-remediation/). Remediated versions use a `+cgr.N` local-version suffix, and Python package managers treat those as compatible higher-precedence replacements for the base version. In addition, CUDA-enabled Python libraries use separate CUDA-specific indexes such as `https://libraries.cgr.dev/cu128/simple/`, and they are not dependency-complete for NVIDIA toolkit components.  



**pip**



If you configured authentication via `.netrc` or the Python keyring provider, run the following commands to set the remediated repository first, then the simple repository:

```bash
pip config set global.index-url https://libraries.cgr.dev/python-remediated/simple/
pip config set global.extra-index-url https://libraries.cgr.dev/python/simple/
```

If you configured authentication via pull token credentials in `pip.conf`, this step
is already done; the `index-url` you set in `.pip/pip.conf` already points
at Chainguard with credentials embedded.




**uv**



Edit the `pyproject.toml` for project-level or the `~/.config/uv/uv.toml` to make global-level changes, setting the remediated repository first:

```toml
[tool.uv]
index-strategy = "unsafe-best-match"

[[tool.uv.index]]
name = "cgr-pr"
url = "https://libraries.cgr.dev/python-remediated/simple"
authenticate = "always"

[[tool.uv.index]]
name = "cgr-p"
url = "https://libraries.cgr.dev/python/simple"
authenticate = "always"
default = true
```

When using the remediated index, set `index-strategy = "unsafe-best-match"` so uv can resolve dependencies that fall back from remediated to non-remediated packages.

Setting `default = true` on the Chainguard index replaces PyPI as uv's default index. Without it, uv appends PyPI as an implicit lowest-priority index, and with `unsafe-best-match` it can resolve packages from PyPI even when Chainguard serves them. This bypasses Chainguard malware blocking and cooldown. If you need packages that Chainguard has not built, use the [built-in upstream fallback](/chainguard/libraries/introduction/overview/#upstream-fallback-and-controls) instead of adding PyPI to your configuration.

If you are using the Python keyring, enable support for it in `pyproject.toml`:

```toml
[tool.uv]
keyring-provider = "subprocess"
```




**Poetry**



Configure the Chainguard sources, setting the remediated index as the primary:

```shell
poetry source add --priority=primary chainguard-remediated https://libraries.cgr.dev/python-remediated/simple/
poetry source add chainguard https://libraries.cgr.dev/python/simple/
```







### Repository manager

If your organization uses a repository manager, configure Chainguard Libraries as an upstream source in that proxy first. Follow the [global configuration documentation](/chainguard/libraries/python/global-configuration/) for your repository manager.

Once configured, point your build tool at your repository manager URL instead of `libraries.cgr.dev` directly. In this setup, the credentials are your repository manager credentials — not a Chainguard pull token.



**pip**



```shell
pip config set global.index-url https://<your-repo-manager-url>/repository/python-all/simple/
```




**uv**



Configure `~/.config/uv/uv.toml` for a global default across all projects:

```bash
[[tool.uv.index]]
   name = "chainguard"
   url = "https://<your-repo-manager-url>/repository/python-all/simple/"
   default = true
```

For a per-project configuration, configure the `pyproject.toml`:

```pyproject.toml
[[tool.uv.index]]
name = "python-all"
url = "https://repo.example.com/repository/python-all/simple/"
default = true
```




**Poetry**



Run the following command:

```shell
poetry config http-basic.python-all REPO_MANAGER_USERNAME REPO_MANAGER_PASSWORD
poetry source add python-all https://repo.example.com/repository/python-all/simple/
```







Example URLs by repository manager:

| Repository manager | URL pattern |
| ----- | ----- |
| JFrog Artifactory | `https://example.jfrog.io/artifactory/api/pypi/python-all/simple/` |
| Sonatype Nexus | `https://repo.example.com:8443/repository/python-all/simple/` |
| Google Artifact Registry | `https://<location>-python.pkg.dev/<project>/python-all/simple/` |
| Cloudsmith | `https://dl.cloudsmith.io/.../<org>/python-all/python/simple/` |

For authentication details specific to your repository manager, refer to the [global configuration documentation](/chainguard/libraries/python/global-configuration/).

## Step 2: Update your lockfile

Your existing lockfile or hash-pinned `requirements.txt` contains checksums generated against packages downloaded from PyPI or through your repository manager. Because Chainguard rebuilds packages from verified source as well as providing security controls for upstream artifacts, checksums differ even for identical version numbers. If your file uses `--hash` entries or `--require-hashes`, installation will fail with a hash mismatch after switching to Chainguard until these are updated.

You can update your lockfile in one of two ways. Update the checksums in place to keep your existing pinned versions, or regenerate the lockfile if you also want to refresh your dependency versions.



**Update in place**



Use `chainctl libraries update-hashes` to rewrite only the integrity hashes in your existing lockfile or requirements file to match Chainguard's artifacts, without re-resolving your dependency graph. Supported formats include `requirements.txt`, `poetry.lock`, `uv.lock`, `pdm.lock`, `Pipfile.lock`, and `pylock.toml`.

Run the following command to auto-detect and update the lockfile in the current project:

```bash
chainctl libraries update-hashes
```

> **Note**: Running `chainctl libraries update-hashes` requires the `libraries.python.pull` permission or the Owner role.

Or specify the lockfile when running the command. For example:

```bash
chainctl libraries update-hashes requirements.txt
```

When using a repo manager, run a command similar to the following:

```bash
chainctl libraries update-hashes
  --registry-url https://repo.example.com/repository/python-all/
  --token "$REPO_TOKEN"
```

By default, Chainguard hashes are appended alongside your existing hashes rather than replacing them. If your installer fails on a dual-hash entry, use the `--replace` flag.




**Regenerate lockfile**



Regenerating the lockfile is another valid approach, and many teams use the migration as an opportunity to update dependencies at the same time. Before you regenerate, consider the following:

* Pinning versions is a security best practice. Regenerating re-resolves your dependencies and can change versions, so you lose your existing pins unless you re-pin afterward.
* Resolvers pick the newest version that satisfies each constraint. Whether Chainguard has that version available depends on the [cooldown
period you've configured](/chainguard/libraries/introduction/overview/#cooldown-period): a release still inside your cooldown window isn't available yet and returns a 404 error.

To regenerate — for example, to intentionally refresh your dependency versions — use the following commands.

pip:

```bash
pip-compile --generate-hashes --index-url https://libraries.cgr.dev/python/simple/ requirements.in
```

uv:

```bash
rm -f uv.lock
uv sync
```

Poetry 1.x:

```bash
poetry lock --no-update
```

Poetry 2.x:

```bash
poetry lock
```







## Step 3: Clear caches

Reinstalling after switching indexes can silently reuse a cached artifact
from your previous index, with no error indicating this happened. To avoid this, clear caches.



**pip**



```shell
pip cache purge
```




**uv**



```bash
uv cache clean
```




**Poetry**



```bash
poetry cache clear --all pypi
```







Also invalidate cached packages in any repository manager that previously proxied PyPI, and rebuild containers without cached layers if your Dockerfile installs dependencies during build:

```shell
docker build --no-cache .
```

## Step 4: Reinstall dependencies

Reinstall dependencies and confirm that the lockfile reflects Chainguard as the source.



**pip**



```shell
pip install --force-reinstall --no-cache-dir -r requirements.txt
```




**uv**



```bash
uv sync --reinstall
```




**Poetry**



```bash
poetry env remove --all
poetry install
```







## Step 5: Verify your libraries

After reinstalling, you can use `chainctl` to verify which dependencies are built by Chainguard. When upstream fallback is enabled, [libraries that aren't built by Chainguard](#packages-not-available-in-chainguard-libraries) are subject to Chainguard's security controls.



**pip and uv**



```shell
chainctl libraries verify --detailed ./.venv/
```




**Poetry**



Poetry's `virtualenv` location depends on your configuration. If `virtualenvs.in-project` is enabled, it's `.venv` in the project directory. Otherwise, find it with:

```bash
poetry env info --path
```

Then:

```bash
chainctl libraries verify --detailed $(poetry env info --path)
```







> **Note**: Running `chainctl libraries verify` requires the `libraries.python.pull` permission or the Owner role.

A successful result shows what percentage of your project's dependencies were built by Chainguard.

For full details on verification options and output, check out [Verification: Analyze a Python wheel file](/chainguard/libraries/policies-and-security/verification/#analyze-a-python-wheel-file).

## Step 6: Commit and roll out

Commit the updated lockfile and any non-sensitive configuration changes. Apply the same index, cache, and hash-update steps to other developer workstations and build servers as you migrate them, including CI/CD platforms and any infrastructure that builds the application or installs dependencies.

For organization-wide rollout using a repository manager, refer to the [global configuration documentation](/chainguard/libraries/python/global-configuration/).

## Packages not available in Chainguard Libraries

Chainguard Libraries covers a large and growing collection of PyPI packages, but not every package or version is available. If a package is missing, your install will fail with a 404 unless you have configured [upstream fallback](/chainguard/libraries/introduction/overview/#upstream-fallback-and-controls).

With upstream fallback enabled, packages not yet available from Chainguard are proxied from PyPI, subject to Chainguard's security controls. Confirm your current policy with:

```shell
chainctl libraries entitlements list
```

For repository manager setups, Chainguard recommends using the configurable fallback rather than configuring a separate public registry fallback in your repository manager, to preserve Chainguard’s security controls.

Learn more about upstream fallback configurations in the [Libraries overview](/chainguard/libraries/introduction/overview/#upstream-fallback-and-controls).

## Troubleshooting

### "403 Forbidden" from the index despite a working keyring

Check for an old or incorrectly scoped `.netrc` entry for `libraries.cgr.dev`, which silently takes priority over keyring authentication. Run with full verbosity to confirm the credential source:

```shell
pip install -vvv <package> 2>&1 | grep -B2 -A2 "credentials\|40[13]"
```

### A pinned dependency shows "not verified" after running `chainctl libraries verify`

Check your install output for a local wheel build (`Building wheel for <package>`), which indicates Chainguard has only a source distribution for that specific pinned version.

### Handling coverage gaps

Coverage gaps can occur at more than one level. For example, a package name may not be present in the Chainguard catalog, the pinned version may not be available, a wheel may. be available for Linux but not for macOS or Windows, or a transitive dependency introduced by a version change may have different coverage from the direct dependency that caused the change.

Do not use a single successful install as proof that the migration is complete. Test each environment that installs the project.

A direct dependency can resolve successfully while one of its transitive dependencies, optional extras, or platform-specific wheels comes from upstream fallback. The reverse can also happen: the direct package may use fallback while its transitive dependencies are available as Chainguard-built artifacts. Evaluate the resolved artifact set, not only the names in `pyproject.toml` or `requirements.in`.

Choose how to handle coverage gaps:

* Confirm whether the failure is authentication or coverage. A 401/403 is an empty version list, and a genuine 404 has different causes.
* Check the exact package version and wheel tags required by the failing environment.
* If the artifact is not available from Chainguard, use the configured protected upstream fallback when policy allows it.
* If the project cannot use fallback, choose a compatible version or move the install to a supported Linux environment only when that is an acceptable product decision.
* Do not silently replace a package with a different version during migration; treat it as a separate depdendency change and test it separately.

## Next steps

* To apply this configuration across your whole organization using a repository manager, refer to the [global configuration](/chainguard/libraries/python/global-configuration/) documentation.  
* To verify downloaded packages were built by Chainguard, refer to the [verification](/chainguard/libraries/policies-and-security/verification/) documentation.  
* For full per-tool configuration reference, refer to the [build configuration](/chainguard/libraries/python/build-configuration/) documentation.
* To keep pinned versions stable when Chainguard publishes new builds, refer to the [build pinning](/chainguard/libraries/policies-and-security/build-pinning/) documentation.

