Skip to main content

Publishing to PyPI: Complete CLI Distribution Guide

Publishing to PyPI makes your CLI tool available to millions of Python developers worldwide. Once on PyPI, users install your tool with a single command: pip install mytool. This article covers the complete release workflow—preparing your package, creating a PyPI account, publishing releases, and automating the process.

Preparing Your Package for Release​

Before publishing, ensure your package is ready:

# Install release tools
pip install build twine

# Build distribution files
python -m build

# Check files are valid
twine check dist/*

# List files to verify
ls -la dist/

The twine check command validates your package files. Fix any errors before proceeding. You should see two files: a wheel (.whl) and a source distribution (.tar.gz).

Creating a PyPI Account​

Register on PyPI:

  1. Visit https://pypi.org/account/register/
  2. Create an account with email and password.
  3. Enable two-factor authentication on your account (required for publishing).
  4. Create an API token at https://pypi.org/manage/account/token/

Store the token securely. You'll use it for publishing without entering your password each time.

Uploading to PyPI​

Upload your package using twine:

# Publish to PyPI (requires token)
twine upload dist/*

# When prompted, use username: __token__
# And password: your-api-token-here

# Or pass token as environment variable
TWINE_PASSWORD="pypi-AgEIc..." twine upload dist/*

After upload, your package is live on PyPI. Users can install it immediately:

pip install mytool

Creating a .pypirc Configuration File​

Store credentials securely instead of passing them on the command line:

# ~/.pypirc (Unix/Mac) or %APPDATA%\pip\pip.ini (Windows)
[distutils]
index-servers =
pypi
testpypi

[pypi]
repository = https://upload.pypi.org/legacy/
username = __token__
password = pypi-AgEIc...

[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-AgEI...

Set file permissions to restrict access: chmod 600 ~/.pypirc. Now publish without credentials on the command line.

Publishing to TestPyPI First​

Test your package on TestPyPI before publishing to production:

# Upload to test server
twine upload -r testpypi dist/*

# Install from test server to verify
pip install --index-url https://test.pypi.org/simple/ mytool

TestPyPI is a safe sandbox. Fix any issues here before publishing to the live PyPI.

Semantic Versioning and Release Tags​

Follow semantic versioning for releases:

MAJOR.MINOR.PATCH

1.0.0 - Initial release (MAJOR=1, MINOR=0, PATCH=0)
1.1.0 - Added features (increment MINOR)
1.1.1 - Bug fix (increment PATCH)
2.0.0 - Breaking changes (increment MAJOR, reset MINOR/PATCH)

Tag releases in git:

# Update version in __init__.py
# commit to git
git add .
git commit -m "chore: bump version to 1.1.0"
git tag -a v1.1.0 -m "Release version 1.1.0"
git push origin v1.1.0

Tags help users find specific release commits and enable automated workflows.

Automating Releases with GitHub Actions​

Automate publishing when you push a tag:

# .github/workflows/publish.yml
name: Publish to PyPI

on:
push:
tags:
- 'v*'

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install build twine

- name: Build distribution
run: python -m build

- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload dist/*

Store your PyPI API token as a GitHub secret. When you push a tag, GitHub automatically builds and publishes your package.

Managing Releases and Updates​

After your first release, manage subsequent versions:

# Increment version in __init__.py
# Edit CHANGELOG.md with new features
# Commit and tag
git add mytool/__init__.py CHANGELOG.md
git commit -m "chore: bump version to 1.1.0"
git tag -a v1.1.0 -m "Release version 1.1.0"
git push origin main
git push origin v1.1.0

# GitHub Actions automatically publishes

Users upgrade with: pip install --upgrade mytool

Comparison Table: PyPI Distribution Channels​

ChannelUse CaseAudienceRisk
TestPyPIPre-release testingDevelopersNone
PyPIProduction releasesEveryoneLive
Git releasesVersion controlDevelopersLow
Automated CI/CDZero-manual stepsTeamReliability

Key Takeaways​

  • Build wheels and source distributions with python -m build.
  • Validate with twine check before uploading.
  • Create a PyPI account and generate an API token for secure publishing.
  • Test on TestPyPI before publishing to production PyPI.
  • Use semantic versioning (MAJOR.MINOR.PATCH) for releases.
  • Automate publishing with GitHub Actions when tags are pushed.
  • Store API tokens as GitHub secrets, never in code.

Frequently Asked Questions​

Can I update a package after publishing?​

You cannot replace a version once published. Increment the version number and publish a new release instead. This preserves reproducibility and user installations.

How long does it take for my package to appear on PyPI?​

Usually instant (seconds). PyPI replicates to mirrors (PyPI CDN) within minutes. Users worldwide can install immediately.

What if I publish with a mistake?​

Immediately publish a fixed version (e.g., 1.0.1). Mark the broken version as yanked on PyPI web interface so new users avoid it. Existing installations remain unchanged.

Can I delete a package from PyPI?​

Only within 5 minutes of publishing. After that, you can only yank versions. Once a package is public, deleting it breaks user workflows, so PyPI discourages it.

Add to your pyproject.toml:

[project.urls]
Changelog = "https://github.com/yourusername/mytool/blob/main/CHANGELOG.md"
Documentation = "https://mytool.readthedocs.io"

These appear on your PyPI package page.

Further Reading​