Publishing to PyPI
Publishing uploads your built wheel and sdist to the Python Package Index. TestPyPI provides a sandbox; trusted publishing uses OIDC tokens instead of passwords.
Recipe
uv build
uv publish # uses PyPI token from envexport UV_PUBLISH_TOKEN=pypi-AgEIcH...
uv publish --index pypiWhen to reach for this:
- Sharing a library with the Python community or other teams
- Distributing internal packages to a private index
- Automating releases from CI
Working Example
# 1. Build
uv build
ls dist/
# 2. Test on TestPyPI
uv publish --index testpypi
uv pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ myapp
# 3. Publish to production PyPI
uv publish# GitHub Actions trusted publishing (no token in secrets)
permissions:
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv build
- uses: pypa/gh-action-pypi-publish@release/v1What this demonstrates:
- Build before publish (never publish source tree directly)
- TestPyPI validates metadata and install before production
- Trusted publishing uses GitHub OIDC - no long-lived tokens
uv publishwraps twine with project-aware defaults
Deep Dive
Publish Checklist
- Version bumped in pyproject.toml
- CHANGELOG.md updated
uv buildsucceedspip install dist/*.whlworks in clean venv- TestPyPI publish and install verified
- Production PyPI publish
- Git tag pushed
Authentication
| Method | Security | Setup |
|---|---|---|
| Trusted publishing | Best | PyPI project settings + GitHub OIDC |
| API token | Good | pypi- prefixed token in env var |
| Password | Deprecated | Do not use |
Gotchas
- Publishing without version bump - PyPI rejects duplicate version. Fix: bump version first.
- Token in source code or CI logs - leaked credentials. Fix: env vars or trusted publishing only.
- Wrong package name - typosquatting risk or name taken. Fix: verify name on PyPI before first publish.
- Missing README/license metadata - poor PyPI page. Fix:
readme = "README.md"andlicensein[project]. - Publishing sdist without wheel - slow installs for consumers. Fix: always publish both wheel and sdist.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Private index | Internal packages | Public open-source |
| Git URL install | Unpublished internal | Reusable library |
| Docker image | Application deploy | Library distribution |
FAQs
twine or uv publish?
uv publish for uv projects. twine works universally: twine upload dist/*.
How do I get a PyPI account?
Register at pypi.org. Enable 2FA. Create project for trusted publishing.
What is TestPyPI?
test.pypi.org - sandbox registry. Separate account and tokens.
Can I unpublish?
Yank (hide) yes. Permanent delete only within 24 hours and rarely.
How do I claim a package name?
First publish wins. Check availability before branding.
How do I publish from CI?
Trusted publishing (recommended) or UV_PUBLISH_TOKEN in secrets.
What files get uploaded?
Everything in dist/ - typically one .whl and one .tar.gz.
How do I add a PyPI description?
readme = "README.md" in pyproject.toml renders on the PyPI page.
Classifiers?
classifiers = ["Programming Language :: Python :: 3.14"] in [project].
How often to publish?
On meaningful changes. Libraries: semver releases. Apps: as needed.
Related
- Packaging Basics - build first
- Versioning & Changelogs - version bumps
- Private Indexes & Mirrors - internal PyPI
- Packaging Best Practices - release hygiene
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.