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:
- Visit https://pypi.org/account/register/
- Create an account with email and password.
- Enable two-factor authentication on your account (required for publishing).
- 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
| Channel | Use Case | Audience | Risk |
|---|---|---|---|
| TestPyPI | Pre-release testing | Developers | None |
| PyPI | Production releases | Everyone | Live |
| Git releases | Version control | Developers | Low |
| Automated CI/CD | Zero-manual steps | Team | Reliability |
Key Takeaways
- Build wheels and source distributions with
python -m build. - Validate with
twine checkbefore 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.
How do I add release notes and documentation links?
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.