Skip to content

Episode 5: Building & DistributionΒΆ

Learning Objectives

By the end of this episode, you will:

  • Build wheel and source distributions
  • Understand semantic versioning
  • Publish packages to PyPI
  • Set up GitHub Actions for CI/CD
  • Create basic documentation
  • Understand package metadata and README rendering

🎬 Taking kir-pydemo to the World¢

Sarah's kir-pydemo package is polished, tested, and ready! But how does she share it?

"I want colleagues at other institutions to use this. How do I get it on PyPI so they can just pip install kir-pydemo? And how do I make sure each release is properly tested?"

The solution? Build distributions and set up automated publishing!

πŸ“¦ Understanding Package DistributionΒΆ

Types of DistributionsΒΆ

Python packages can be distributed in two formats:

1. Source Distribution (sdist)ΒΆ

A compressed archive (.tar.gz) of your source code:

kir-pydemo-0.1.0.tar.gz
β”œβ”€β”€ src/
β”‚   └── kir_pydemo/
β”œβ”€β”€ tests/
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ README.md
└── LICENSE
  • Pros: Works on any platform, includes everything
  • Cons: Requires build tools, slower to install
  • When: Fallback when wheels aren't available

2. Wheel (.whl)ΒΆ

A pre-built package ready for installation:

kir_pydemo-0.1.0-py3-none-any.whl
  • Pros: Fast installation, no build required
  • Cons: May need platform-specific builds (for C extensions)
  • When: Preferred format for distribution

The filename tells you:

kir_pydemo - 0.1.0 - py3 - none - any .whl
    |         |      |     |     |
  package  version  py3  no ABI universal
                         (pure Python)

πŸ—οΈ Building Your PackageΒΆ

Step 1: Install Build ToolsΒΆ

uv pip install build twine
  • build: Creates wheels and sdist
  • twine: Uploads packages to PyPI

Step 2: Build the DistributionsΒΆ

# Build both wheel and source distribution
python -m build

# Output:
# Successfully built kir_pydemo-0.1.0.tar.gz and kir_pydemo-0.1.0-py3-none-any.whl

This creates a dist/ directory:

dist/
β”œβ”€β”€ kir_pydemo-0.1.0-py3-none-any.whl
└── kir_pydemo-0.1.0.tar.gz

Step 3: Check the BuildΒΆ

# Verify the distribution
twine check dist/*

# Output:
# Checking dist/kir_pydemo-0.1.0-py3-none-any.whl: PASSED
# Checking dist/kir_pydemo-0.1.0.tar.gz: PASSED

Step 4: Test Install LocallyΒΆ

# Create a fresh virtual environment
python -m venv test-env
source test-env/bin/activate

# Install from the wheel
pip install dist/kir_pydemo-0.1.0-py3-none-any.whl

# Test it works
kir-pydemo gc-content ATGC
# Output: GC content: 50.00%

# Deactivate and cleanup
deactivate
rm -rf test-env

πŸ”’ Semantic VersioningΒΆ

Version numbers communicate compatibility and changes:

MAJOR.MINOR.PATCH
  |     |     |
  |     |     └─ Bug fixes (backwards compatible)
  |     └─────── New features (backwards compatible)
  └───────────── Breaking changes (NOT backwards compatible)

ExamplesΒΆ

  • 0.1.0 β†’ 0.1.1: Fixed a bug
  • 0.1.1 β†’ 0.2.0: Added new feature (reverse_complement)
  • 0.2.0 β†’ 1.0.0: First stable release
  • 1.0.0 β†’ 2.0.0: Changed API (breaking change)

Pre-releasesΒΆ

1.0.0a1   # Alpha release 1
1.0.0b1   # Beta release 1
1.0.0rc1  # Release candidate 1
1.0.0     # Final release

Version in pyproject.tomlΒΆ

Static version:

[project]
version = "0.1.0"

Dynamic version (from code):

[project]
dynamic = ["version"]

[tool.setuptools.dynamic]
version = {attr = "kir_pydemo.__version__"}

Then in src/kir_pydemo/__init__.py:

__version__ = "0.1.0"

Version Management Tools

  • bump2version: CLI tool to bump versions
  • poetry version: Poetry's version management
  • setuptools_scm: Git tag-based versioning

Example with setuptools_scm:

[build-system]
requires = ["setuptools>=61.0", "setuptools-scm"]

[project]
dynamic = ["version"]

[tool.setuptools_scm]

πŸš€ Publishing to PyPIΒΆ

Test on TestPyPI FirstΒΆ

TestPyPI is a separate instance for testing:

# Create account on https://test.pypi.org/account/register/

# Upload to TestPyPI
twine upload --repository testpypi dist/*

# Install from TestPyPI to verify
pip install --index-url https://test.pypi.org/simple/ kir-pydemo

Publish to PyPIΒΆ

# Create account on https://pypi.org/account/register/

# Upload to PyPI
twine upload dist/*

# Enter your credentials or use API token

Package Name Availability

Check if the name is available first:

pip search kir-pydemo  # Deprecated, use web search instead
Visit https://pypi.org/project/ to check.

Create an API token on PyPI:

  1. Go to https://pypi.org/manage/account/
  2. Create a new API token
  3. Store it safely

Option 1: .pypirc file

Create ~/.pypirc:

[pypi]
username = __token__
password = pypi-AgEIcHlwaS5vcmc...

[testpypi]
username = __token__
password = pypi-AgENdGVzdC5weXBp...

Option 2: Environment variable

export TWINE_USERNAME=__token__
export TWINE_PASSWORD=pypi-AgEIcHlwaS5vcmc...

twine upload dist/*

πŸ€– Continuous Integration with GitHub ActionsΒΆ

Automate testing and publishing with GitHub Actions:

Step 1: Create Workflow DirectoryΒΆ

mkdir -p .github/workflows

Step 2: Create CI WorkflowΒΆ

Create .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ['3.9', '3.10', '3.11', '3.12']

    steps:
    - uses: actions/checkout@v4

    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -e ".[dev]"

    - name: Lint with ruff
      run: |
        ruff check src/ tests/

    - name: Type check with mypy
      run: |
        mypy src/

    - name: Test with pytest
      run: |
        pytest --cov=kir_pydemo --cov-report=xml

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.xml
        fail_ci_if_error: true

Step 3: Create Release WorkflowΒΆ

Create .github/workflows/release.yml:

name: Release

on:
  release:
    types: [created]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

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

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

    - name: Build package
      run: python -m build

    - name: Check package
      run: twine check dist/*

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

Step 4: Add PyPI Token to GitHubΒΆ

  1. Generate API token on PyPI
  2. Go to your GitHub repo β†’ Settings β†’ Secrets and variables β†’ Actions
  3. Add new secret: PYPI_API_TOKEN

Now when you create a release on GitHub, it automatically publishes to PyPI!

πŸ“‹ Complete Checklist for ReleaseΒΆ

Before publishing your package:

Code QualityΒΆ

  • All tests passing
  • Code coverage >80%
  • No linting errors
  • Type checking passes
  • Pre-commit hooks configured

DocumentationΒΆ

  • Comprehensive README.md
  • Usage examples
  • Installation instructions
  • API documentation
  • Changelog or release notes

MetadataΒΆ

  • Appropriate version number
  • Correct dependencies and version constraints
  • Proper license
  • Keywords and classifiers
  • Project URLs (homepage, docs, issues)

TestingΒΆ

  • Tested on multiple Python versions
  • Tested on different platforms (if relevant)
  • Test installation from built wheel

SecurityΒΆ

  • No sensitive data in code
  • Dependencies checked for vulnerabilities
  • API tokens secured (not in git)
  • LICENSE file included
  • All code properly attributed
  • Dependencies' licenses compatible

πŸ“‹ Checkpoint: What Have We Achieved?ΒΆ

Verify you've successfully completed Episode 5:

  • Built wheel and source distributions with python -m build
  • Verified distributions with twine check
  • Understand semantic versioning (MAJOR.MINOR.PATCH)
  • Published to TestPyPI successfully
  • Set up GitHub Actions for CI
  • Created comprehensive README with badges
  • Configured automated release workflow

🎯 Key Takeaways¢

  1. Two distribution formats: Wheels (preferred) and source distributions (fallback)
  2. Semantic versioning communicates compatibility: MAJOR.MINOR.PATCH
  3. Test on TestPyPI before publishing to PyPI
  4. Use API tokens for secure authentication
  5. GitHub Actions automate testing and publishing
  6. Good documentation is crucial for adoption
  7. Release checklist ensures quality releases

πŸŽ“ Series Wrap-UpΒΆ

Congratulations! You've completed the Python Packaging Basics series. You now know how to:

βœ… Structure packages with modern practices (src/ layout, pyproject.toml)
βœ… Build CLI tools with entry points
βœ… Manage dependencies and environments
βœ… Test and maintain code quality
βœ… Distribute packages to PyPI

What You've BuiltΒΆ

The kir-pydemo package now:

  • Has a clean, modern project structure
  • Provides both Python API and CLI
  • Manages dependencies properly
  • Includes comprehensive tests
  • Follows code quality standards
  • Can be published to PyPI
  • Has automated CI/CD

Next StepsΒΆ

  • Apply to your projects: Package your own tools
  • Explore advanced topics: C extensions, binary wheels, conda packages
  • Join the community: Contribute to open source
  • Keep learning: Python packaging evolves - stay updated!

πŸ“š Further ReadingΒΆ