Private Indexes & Alternative Distribution

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the Python Reference bibliography.

PyPI has no private projects: everything uploaded there is public, permanently. Internal libraries go to a private index, which pip and Poetry use exactly like PyPI because it speaks the same simple repository API (PEP 503/691). This page shows the self-hosted options run on this machine on 2026-10-22 (pypiserver 2.4.2, devpi-server 6.20.3 with devpi-client 7.3.0, pip 26.2.2, all on Linux with CPython 3.14.6) and lists the hosted ones, which are not run here.

What an index needs to be

A package index is just HTTP: /simple/ lists projects, /simple/<normalised-name>/ lists files with their hashes. The simplest private index is therefore a directory of static files behind any web server:

simple/index.html                     <a href="birdtally/">birdtally</a>
simple/birdtally/index.html           <a href="../../files/birdtally-0.1.0-py3-none-any.whl#sha256=…">…</a>
files/birdtally-0.1.0-py3-none-any.whl

That layout, served with python -m http.server 8085, was enough for pip to install from --index-url http://127.0.0.1:8085/simple/ (and to honour a data-yanked attribute, see Managing Published Releases). Serve it over HTTPS in practice, and generate it with a tool rather than by hand; uploads need a real server.

pypiserver

pypiserver serves a directory of distribution files and accepts uploads. Every local round trip in this section used it:

python -m pip install pypiserver==2.4.2 passlib
htpasswd -sc htpasswd birdwatch                    # or passlib's HtpasswdFile
pypi-server run -p 8084 -a update -P htpasswd --disable-fallback packages/
  • -a update requires authentication for uploads only (-a download,list,update for everything); -P htpasswd names the password file.

  • --disable-fallback makes it answer 404 for unknown projects. Without it, pypiserver redirects requests for projects it does not have to PyPI, so a supposedly private index silently serves public packages (the trap seen on Repositories & Package Sources).

  • Upload with twine (--repository-url http://localhost:8084/) or Poetry (poetry config repositories.local …); install with --index-url http://localhost:8084/simple/. Both are shown on Publishing with build & twine and Publishing with Poetry.

  • A re-upload is refused (already exists! You may start server with --overwrite option.).

Source: pypiserver.

devpi

devpi adds what pypiserver lacks: users, several indexes, a caching mirror of PyPI (root/pypi) and index inheritance, so one URL serves private and public packages with the private ones taking precedence:

devpi-init --serverdir devpi-data
devpi-server --serverdir devpi-data --host 127.0.0.1 --port 3141 &
devpi use http://127.0.0.1:3141
devpi user -c alice password="$DEVPI_PASSWORD" email=alice@example.org
devpi login alice --password "$DEVPI_PASSWORD"
devpi index -c dev bases=root/pypi
devpi use alice/dev
devpi upload --from-dir dist
pip install --dry-run --index-url http://127.0.0.1:3141/alice/dev/+simple/ "birdtally[pretty]"
user created: alice
logged in 'alice', credentials valid for 10.00 hours
file_upload of birdtally-0.1.0-py3-none-any.whl to http://127.0.0.1:3141/alice/dev/
file_upload of birdtally-0.1.0.tar.gz to http://127.0.0.1:3141/alice/dev/
…
Would install Pygments-2.21.0 birdtally-0.1.0 markdown-it-py-4.2.0 mdurl-0.1.2 rich-15.0.0

birdtally came from alice/dev, its dependencies from the cached PyPI mirror. PyPI’s own help recommends devpi for private packages. Once a project exists in alice/dev, devpi stops serving that name from the inherited mirror (unless it is listed in the index’s mirror_whitelist), which protects uploaded private names; names you have not uploaded yet still resolve from PyPI, so register internal names early or use an index without a PyPI base for them. Tried here with a private six 0.0.1 uploaded to alice/dev: pip index versions listed only 0.0.1 there, while root/pypi offered six (1.17.0).

Hosted registries (not run here)

Registry Upload and install

GitLab package registry (all tiers)

Upload with twine to ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi as gitlab-ci-token with $CI_JOB_TOKEN in CI (or a personal or deploy token); install from …/packages/pypi/simple, per project or per group. Uploading the same name and version twice gives 400 Bad Request. Package request forwarding is on by default: a package missing from the registry is fetched from pypi.org even with --index-url, a dependency-confusion vector; group owners can turn it off.

Sonatype Nexus Repository

A hosted PyPI repository for uploads, a proxy repository for PyPI and a group repository combining them for installs.

JFrog Artifactory

Local, remote and virtual PyPI repositories, the same pattern. Its documentation blocks scripted access; read it in a browser.

AWS CodeArtifact

aws codeartifact login --tool twine --domain my_domain --domain-owner 111122223333 --repository my_repo writes a short-lived token into .pypirc; --tool pip configures pip’s index URL.

Azure Artifacts

A feed’s upload URL is https://pkgs.dev.azure.com/ORGANIZATION_NAME/PROJECT_NAME/_packaging/FEED_NAME/pypi/upload/; installing uses …/pypi/simple/ with the artifacts-keyring package providing credentials.

Google Artifact Registry

Python repositories at https://LOCATION-python.pkg.dev/PROJECT/REPOSITORY/; keyrings.google-artifactregistry-auth gives pip and twine the gcloud credentials.

GitHub Packages

Has no Python (PyPI) registry. Use one of the above, or a GitHub release with wheel files.

Mirrors and caches

A caching proxy in front of PyPI (devpi’s root/pypi, a Nexus or Artifactory proxy, or a plain HTTP cache) speeds up CI and keeps builds working when PyPI is unreachable. A full mirror (bandersnatch) copies all of PyPI, which few need. pip’s own cache already helps per machine (see pip: Wheels & Caching).

Without an index: Git and direct URLs

For a few internal consumers, a Git repository can be the distribution channel:

python -m pip install "birdtally @ git+https://git.example.org/birds/birdtally.git@v0.2.0"

Limits: every install builds from source (a compiler for extensions, the build backend from an index); there is no version resolution across releases, only the ref you name; and a package that depends on such a URL cannot be uploaded to PyPI, which rejects direct references in Requires-Dist. Wheels attached to a GitHub or GitLab release, installed by URL or through --find-links, avoid the build step. Details are on pip: VCS, Direct URL & Local Project Installs.

conda-forge is a separate ecosystem with its own recipes, builds and channel; publishing there is a different process (see the conda-forge documentation).

Consuming a private index