How to Publish a Python Package to PyPI (2026)

Build your package, test it on TestPyPI, then publish to PyPI with twine, uv, Hatch, or Poetry, plus trusted publishing from GitHub Actions

Publishing to PyPI means building two files (a source distribution and a wheel), then uploading them with credentials. Test the upload on TestPyPI first, and prefer trusted publishing over long-lived API tokens once you automate it. The steps are the same whichever tool you use. Only the commands change.

Before you start

You need a pyproject.toml with a [build-system] section and your project’s metadata (see requirements.txt vs pyproject.toml if that file is new to you), a name that is not already taken on PyPI, and a PyPI account with two-factor authentication set up. If you are choosing a tool for the project itself, Poetry vs uv vs Hatch and the package manager comparison will help.

Step 1: build the distributions

python3 -m build      # or: uv build

This produces two files in dist/: a wheel (.whl) and a source distribution (.tar.gz). The official packaging tutorial uses exactly this command.

Step 2: try it on TestPyPI

TestPyPI is a separate index for practice uploads, with its own account and API tokens. With twine:

python3 -m twine upload --repository testpypi dist/*

Twine prompts for an API token; paste the token value including its pypi- prefix. With uv, add TestPyPI as an index with a publish-url in pyproject.toml, then run uv publish --index testpypi.

Step 3: publish to PyPI

ToolBuildPublish
pip and twinepython3 -m buildtwine upload dist/*
uvuv builduv publish
Hatchhatch buildhatch publish
Poetrypoetry buildpoetry publish --build

Two details from the tools’ own docs: Hatch publishes the .whl and .tar.gz files in dist/, and Poetry only uploads artifacts of the latest version in dist/, so old builds lying around are ignored.

Use trusted publishing for automation

PyPI’s normal API tokens are long-lived: if one leaks, it works until you notice and revoke it. Trusted publishing replaces them with short-lived tokens exchanged between your CI service and PyPI over OpenID Connect, so there is no secret to store or leak. uv supports it, and will try to invalidate the short-lived token after publishing even if the upload fails.

For GitHub Actions, the packaging guide’s workflow publishes whenever a tagged commit is pushed, using the pypa/gh-action-pypi-publish action. The key requirement is a job permission that is easy to forget:

permissions:
  id-token: write   # mandatory for trusted publishing

You also register your GitHub repository as a trusted publisher in your project’s settings on PyPI. Follow the packaging guide for GitHub Actions workflows for the complete workflow rather than copying a partial one.

Mistakes that cost an afternoon

  • Reusing a version. PyPI never allows a filename to be reused, even after you delete the release. A bad upload means a new version number, which is a good reason to test on TestPyPI first.
  • A README that will not render. PyPI rejects uploads whose description fails to render. Run twine check dist/* before uploading.
  • Stale files in dist/. Clear old builds, or you may upload the wrong versions (Poetry guards against this; twine does not).
  • Leaving long-lived tokens in CI. Once the first manual release works, switch to trusted publishing.

Frequently Asked Questions

How do I publish a Python package to PyPI?

Build a source distribution and a wheel (python -m build, or uv build), then upload them with twine upload, uv publish, hatch publish, or poetry publish. Authenticate with an API token, or better, with trusted publishing from CI.

What is TestPyPI?

A separate instance of the package index for practice uploads, used in the official packaging tutorial. Try your upload there first, because PyPI does not let you reuse a filename even after the release is deleted.

Can I re-upload the same version to PyPI?

No. PyPI does not allow a distribution filename to be reused, even once a project has been deleted and recreated. If you need to fix a release, bump the version number and upload again.

What is trusted publishing?

A way to publish from CI using short-lived tokens exchanged through OpenID Connect instead of a manually created API token. PyPI’s normal tokens are long-lived, so a leaked token works until someone revokes it, while trusted publishing tokens expire automatically.