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