No. GitHub Packages supports npm, Docker and other container images, Maven, Gradle, NuGet and RubyGems, but not Python. There’s no PyPI-compatible registry, so you can’t twine upload to it or point pip at it.
It’s one of the most requested GitHub features. The community discussion asking for it has been open since December 2021 with hundreds of upvotes, and a reply in the thread says it’s no longer planned. If your code is on GitHub and you want private Python packages, you need to host them somewhere else.
Your options
Install straight from Git. pip install git+https://github.com/your-org/your-lib.git@v1.2.0 works with no extra service. The downsides show up as you grow: pip can’t resolve version ranges across tags, every install clones the repo, and every consumer (including CI) needs read access to the source repository.
GitLab’s package registry. GitLab has a PyPI-compatible registry on every tier, including free. It makes most sense if you’re already on GitLab. Using it from GitHub means a GitLab project and a second set of tokens. By default it also forwards requests for packages it doesn’t have to pypi.org, which you’ll want to understand if you’re worried about dependency confusion.
AWS CodeArtifact. It works, and it’s a natural choice if you’re already deep in AWS. Expect some IAM setup, and its auth tokens last at most 12 hours, so CI and developer machines need to refresh them with aws codeartifact login.
Self-host. pypiserver and devpi are mature and free. You run, back up and secure them yourself.
A hosted registry. Gemfury, Cloudsmith, JFrog Artifactory and RepoForge all give you a private PyPI index you can publish to from GitHub Actions. The rest of this post uses RepoForge, because it’s ours, but the setup is broadly similar elsewhere.
Publishing from GitHub Actions
You need two repository secrets:
REPOFORGE_TOKEN: an API token with push access, created in RepoForgeREPOFORGE_HASH_ID: your registry ID, the part afterapi.repoforge.io/in your registry URL
Then add .github/workflows/publish.yml. This builds and publishes whenever you push a tag starting with v; the action does the build for you:
name: Publish
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: RepoForge-io/repoforge-publish-action@v1.1.1
with:
package_type: python
api_token: ${{ secrets.REPOFORGE_TOKEN }}
hash_id: ${{ secrets.REPOFORGE_HASH_ID }}
package_dir: ./dist
Tag a release and push it:
git tag v1.2.0
git push origin v1.2.0
Installing
Authentication is HTTP basic auth: the username can be anything and the password is your token. Use a read-only token for installs.
pip
pip install your-lib \
--index-url https://token:${REPOFORGE_TOKEN}@api.repoforge.io/<your-id>/
Or set it once in pip.conf (~/.config/pip/pip.conf on Linux and macOS):
[global]
index-url = https://token:<your-token>@api.repoforge.io/<your-id>/
Enable upstream mirroring on your registry, and this single index URL serves both your private packages and everything from PyPI. Then you don’t need --extra-index-url, which is what makes dependency confusion possible.
uv
In pyproject.toml:
[[tool.uv.index]]
name = "repoforge"
url = "https://api.repoforge.io/<your-id>/"
Credentials come from the environment, so they never end up in the file:
export UV_INDEX_REPOFORGE_USERNAME=token
export UV_INDEX_REPOFORGE_PASSWORD=$REPOFORGE_TOKEN
uv add your-lib
Poetry
poetry source add --priority=supplemental repoforge https://api.repoforge.io/<your-id>/
poetry config http-basic.repoforge token $REPOFORGE_TOKEN
poetry add --source repoforge your-lib
In another GitHub Actions workflow
Add a read-only token as a secret and pass it through the environment:
- name: Install dependencies
env:
REPOFORGE_TOKEN: ${{ secrets.REPOFORGE_READ_TOKEN }}
run: |
pip install -r requirements.txt \
--index-url https://token:${REPOFORGE_TOKEN}@api.repoforge.io/<your-id>/
Try it
RepoForge has a 14-day free trial, no card needed. Create an account and you’ll have a registry URL straight away.