--- name: python-packaging description: Create distributable Python packages with proper project structure, setup.py/pyproject.toml, and publishing to PyPI. Use when packaging Python libraries, creating CLI tools, or distributing Python code. --- # Python Packaging Comprehensive guide to creating, structuring, and distributing Python packages using modern packaging tools, pyproject.toml, and publishing to PyPI. ## When to Use This Skill - Creating Python libraries for distribution - Building command-line tools with entry points - Publishing packages to PyPI or private repositories - Setting up Python project structure - Creating installable packages with dependencies - Building wheels and source distributions - Versioning and releasing Python packages - Creating namespace packages - Implementing package metadata and classifiers ## Core Concepts ### 1. Package Structure - **Source layout**: `src/package_name/` (recommended) - **Flat layout**: `package_name/` (simpler but less flexible) - **Package metadata**: pyproject.toml, setup.py, or setup.cfg - **Distribution formats**: wheel (.whl) and source distribution (.tar.gz) ### 2. Modern Packaging Standards - **PEP 517/518**: Build system requirements - **PEP 621**: Metadata in pyproject.toml - **PEP 660**: Editable installs - **pyproject.toml**: Single source of configuration ### 3. Build Backends - **setuptools**: Traditional, widely used - **hatchling**: Modern, opinionated - **flit**: Lightweight, for pure Python - **poetry**: Dependency management + packaging ### 4. Distribution - **PyPI**: Python Package Index (public) - **TestPyPI**: Testing before production - **Private repositories**: JFrog, AWS CodeArtifact, etc. ## Quick Start ### Minimal Package Structure ``` my-package/ ├── pyproject.toml ├── README.md ├── LICENSE ├── src/ │ └── my_package/ │ ├── __init__.py │ └── module.py └── tests/ └── test_module.py ``` ### Minimal pyproject.toml ```toml [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "my-package" version = "0.1.0" description = "A short description" authors = [{name = "Your Name", email = "you@example.com"}] readme = "README.md" requires-python = ">=3.8" dependencies = [ "requests>=2.28.0", ] [project.optional-dependencies] dev = [ "pytest>=7.0", "black>=22.0", ] ``` ## Package Structure Patterns ### Pattern 1: Source Layout (Recommended) ``` my-package/ ├── pyproject.toml ├── README.md ├── LICENSE ├── .gitignore ├── src/ │ └── my_package/ │ ├── __init__.py │ ├── core.py │ ├── utils.py │ └── py.typed # For type hints ├── tests/ │ ├── __init__.py │ ├── test_core.py │ └── test_utils.py └── docs/ └── index.md ``` **Advantages:** - Prevents accidentally importing from source - Cleaner test imports - Better isolation **pyproject.toml for source layout:** ```toml [tool.setuptools.packages.find] where = ["src"] ``` ### Pattern 2: Flat Layout ``` my-package/ ├── pyproject.toml ├── README.md ├── my_package/ │ ├── __init__.py │ └── module.py └── tests/ └── test_module.py ``` **Simpler but:** - Can import package without installing - Less professional for libraries ### Pattern 3: Multi-Package Project ``` project/ ├── pyproject.toml ├── packages/ │ ├── package-a/ │ │ └── src/ │ │ └── package_a/ │ └── package-b/ │ └── src/ │ └── package_b/ └── tests/ ``` ## Complete pyproject.toml Examples ### Pattern 4: Full-Featured pyproject.toml ```toml [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "my-awesome-package" version = "1.0.0" description = "An awesome Python package" readme = "README.md" requires-python = ">=3.8" license = {text = "MIT"} authors = [ {name = "Your Name", email = "you@example.com"}, ] maintainers = [ {name = "Maintainer Name", email = "maintainer@example.com"}, ] keywords = ["example", "package", "awesome"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] dependencies = [ "requests>=2.28.0,<3.0.0", "click>=8.0.0", "pydantic>=2.0.0", ] [project.optional-dependencies] dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", "black>=23.0.0", "ruff>=0.1.0", "mypy>=1.0.0", ] docs = [ "sphinx>=5.0.0", "sphinx-rtd-theme>=1.0.0", ] all = [ "my-awesome-package[dev,docs]", ] [project.urls] Homepage = "https://github.com/username/my-awesome-package" Documentation = "https://my-awesome-package.readthedocs.io" Repository = "https://github.com/username/my-awesome-package" "Bug Tracker" = "https://github.com/username/my-awesome-package/issues" Changelog = "https://github.com/username/my-awesome-package/blob/main/CHANGELOG.md" [project.scripts] my-cli = "my_package.cli:main" awesome-tool = "my_package.tools:run" [project.entry-points."my_package.plugins"] plugin1 = "my_package.plugins:plugin1" [tool.setuptools] package-dir = {"" = "src"} zip-safe = false [tool.setuptools.packages.find] where = ["src"] include = ["my_package*"] exclude = ["tests*"] [tool.setuptools.package-data] my_package = ["py.typed", "*.pyi", "data/*.json"] # Black configuration [tool.black] line-length = 100 target-version = ["py38", "py39", "py310", "py311"] include = '\.pyi?$' # Ruff configuration [tool.ruff] line-length = 100 target-version = "py38" [tool.ruff.lint] select = ["E", "F", "I", "N", "W", "UP"] # MyPy configuration [tool.mypy] python_version = "3.8" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true # Pytest configuration [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] addopts = "-v --cov=my_package --cov-report=term-missing" # Coverage configuration [tool.coverage.run] source = ["src"] omit = ["*/tests/*"] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "raise AssertionError", "raise NotImplementedError", ] ``` ### Pattern 5: Dynamic Versioning ```toml [build-system] requires = ["setuptools>=61.0", "setuptools-scm>=8.0"] build-backend = "setuptools.build_meta" [project] name = "my-package" dynamic = ["version"] description = "Package with dynamic version" [tool.setuptools.dynamic] version = {attr = "my_package.__version__"} # Or use setuptools-scm for git-based versioning [tool.setuptools_scm] write_to = "src/my_package/_version.py" ``` **In __init__.py:** ```python # src/my_package/__init__.py __version__ = "1.0.0" # Or with setuptools-scm from importlib.metadata import version __version__ = version("my-package") ``` ## Command-Line Interface (CLI) Patterns ### Pattern 6: CLI with Click ```python # src/my_package/cli.py import click @click.group() @click.version_option() def cli(): """My awesome CLI tool.""" pass @cli.command() @click.argument("name") @click.option("--greeting", default="Hello", help="Greeting to use") def greet(name: str, greeting: str): """Greet someone.""" click.echo(f"{greeting}, {name}!") @cli.command() @click.option("--count", default=1, help="Number of times to repeat") def repeat(count: int): """Repeat a message.""" for i in range(count): click.echo(f"Message {i + 1}") def main(): """Entry point for CLI.""" cli() if __name__ == "__main__": main() ``` **Register in pyproject.toml:** ```toml [project.scripts] my-tool = "my_package.cli:main" ``` **Usage:** ```bash pip install -e . my-tool greet World my-tool greet Alice --greeting="Hi" my-tool repeat --count=3 ``` ### Pattern 7: CLI with argparse ```python # src/my_package/cli.py import argparse import sys def main(): """Main CLI entry point.""" parser = argparse.ArgumentParser( description="My awesome tool", prog="my-tool" ) parser.add_argument( "--version", action="version", version="%(prog)s 1.0.0" ) subparsers = parser.add_subparsers(dest="command", help="Commands") # Add subcommand process_parser = subparsers.add_parser("process", help="Process data") process_parser.add_argument("input_file", help="Input file path") process_parser.add_argument( "--output", "-o", default="output.txt", help="Output file path" ) args = parser.parse_args() if args.command == "process": process_data(args.input_file, args.output) else: parser.print_help() sys.exit(1) def process_data(input_file: str, output_file: str): """Process data from input to output.""" print(f"Processing {input_file} -> {output_file}") if __name__ == "__main__": main() ``` ## Building and Publishing ### Pattern 8: Build Package Locally ```bash # Install build tools pip install build twine # Build distribution python -m build # This creates: # dist/ # my-package-1.0.0.tar.gz (source distribution) # my_package-1.0.0-py3-none-any.whl (wheel) # Check the distribution twine check dist/* ``` ### Pattern 9: Publishing to PyPI ```bash # Install publishing tools pip install twine # Test on TestPyPI first twine upload --repository testpypi dist/* # Install from TestPyPI to test pip install --index-url https://test.pypi.org/simple/ my-package # If all good, publish to PyPI twine upload dist/* ``` **Using API tokens (recommended):** ```bash # Create ~/.pypirc [distutils] index-servers = pypi testpypi [pypi] username = __token__ password = pypi-...your-token... [testpypi] username = __token__ password = pypi-...your-test-token... ``` ### Pattern 10: Automated Publishing with GitHub Actions ```yaml # .github/workflows/publish.yml name: Publish to PyPI on: release: types: [created] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.11" - name: Install dependencies run: | 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/* ``` ## Advanced Patterns ### Pattern 11: Including Data Files ```toml [tool.setuptools.package-data] my_package = [ "data/*.json", "templates/*.html", "static/css/*.css", "py.typed", ] ``` **Accessing data files:** ```python # src/my_package/loader.py from importlib.resources import files import json def load_config(): """Load configuration from package data.""" config_file = files("my_package").joinpath("data/config.json") with config_file.open() as f: return json.load(f) # Python 3.9+ from importlib.resources import files data = files("my_package").joinpath("data/file.txt").read_text() ``` ### Pattern 12: Namespace Packages **For large projects split across multiple repositories:** ``` # Package 1: company-core company/ └── core/ ├── __init__.py └── models.py # Package 2: company-api company/ └── api/ ├── __init__.py └── routes.py ``` **Do NOT include __init__.py in the namespace directory (company/):** ```toml # company-core/pyproject.toml [project] name = "company-core" [tool.setuptools.packages.find] where = ["."] include = ["company.core*"] # company-api/pyproject.toml [project] name = "company-api" [tool.setuptools.packages.find] where = ["."] include = ["company.api*"] ``` **Usage:** ```python # Both packages can be imported under same namespace from company.core import models from company.api import routes ``` ### Pattern 13: C Extensions ```toml [build-system] requires = ["setuptools>=61.0", "wheel", "Cython>=0.29"] build-backend = "setuptools.build_meta" [tool.setuptools] ext-modules = [ {name = "my_package.fast_module", sources = ["src/fast_module.c"]}, ] ``` **Or with setup.py:** ```python # setup.py from setuptools import setup, Extension setup( ext_modules=[ Extension( "my_package.fast_module", sources=["src/fast_module.c"], include_dirs=["src/include"], ) ] ) ``` ## Version Management ### Pattern 14: Semantic Versioning ```python # src/my_package/__init__.py __version__ = "1.2.3" # Semantic versioning: MAJOR.MINOR.PATCH # MAJOR: Breaking changes # MINOR: New features (backward compatible) # PATCH: Bug fixes ``` **Version constraints in dependencies:** ```toml dependencies = [ "requests>=2.28.0,<3.0.0", # Compatible range "click~=8.1.0", # Compatible release (~= 8.1.0 means >=8.1.0,<8.2.0) "pydantic>=2.0", # Minimum version "numpy==1.24.3", # Exact version (avoid if possible) ] ``` ### Pattern 15: Git-Based Versioning ```toml [build-system] requires = ["setuptools>=61.0", "setuptools-scm>=8.0"] build-backend = "setuptools.build_meta" [project] name = "my-package" dynamic = ["version"] [tool.setuptools_scm] write_to = "src/my_package/_version.py" version_scheme = "post-release" local_scheme = "dirty-tag" ``` **Creates versions like:** - `1.0.0` (from git tag) - `1.0.1.dev3+g1234567` (3 commits after tag) ## Testing Installation ### Pattern 16: Editable Install ```bash # Install in development mode pip install -e . # With optional dependencies pip install -e ".[dev]" pip install -e ".[dev,docs]" # Now changes to source code are immediately reflected ``` ### Pattern 17: Testing in Isolated Environment ```bash # Create virtual environment python -m venv test-env source test-env/bin/activate # Linux/Mac # test-env\Scripts\activate # Windows # Install package pip install dist/my_package-1.0.0-py3-none-any.whl # Test it works python -c "import my_package; print(my_package.__version__)" # Test CLI my-tool --help # Cleanup deactivate rm -rf test-env ``` ## Documentation ### Pattern 18: README.md Template ```markdown # My Package [![PyPI version](https://badge.fury.io/py/my-package.svg)](https://pypi.org/project/my-package/) [![Python versions](https://img.shields.io/pypi/pyversions/my-package.svg)](https://pypi.org/project/my-package/) [![Tests](https://github.com/username/my-package/workflows/Tests/badge.svg)](https://github.com/username/my-package/actions) Brief description of your package. ## Installation ```bash pip install my-package ``` ## Quick Start ```python from my_package import something result = something.do_stuff() ``` ## Features - Feature 1 - Feature 2 - Feature 3 ## Documentation Full documentation: https://my-package.readthedocs.io ## Development ```bash git clone https://github.com/username/my-package.git cd my-package pip install -e ".[dev]" pytest ``` ## License MIT ``` ## Common Patterns ### Pattern 19: Multi-Architecture Wheels ```yaml # .github/workflows/wheels.yml name: Build wheels on: [push, pull_request] jobs: build_wheels: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v3 - name: Build wheels uses: pypa/cibuildwheel@v2.16.2 - uses: actions/upload-artifact@v3 with: path: ./wheelhouse/*.whl ``` ### Pattern 20: Private Package Index ```bash # Install from private index pip install my-package --index-url https://private.pypi.org/simple/ # Or add to pip.conf [global] index-url = https://private.pypi.org/simple/ extra-index-url = https://pypi.org/simple/ # Upload to private index twine upload --repository-url https://private.pypi.org/ dist/* ``` ## File Templates ### .gitignore for Python Packages ```gitignore # Build artifacts build/ dist/ *.egg-info/ *.egg .eggs/ # Python __pycache__/ *.py[cod] *$py.class *.so # Virtual environments venv/ env/ ENV/ # IDE .vscode/ .idea/ *.swp # Testing .pytest_cache/ .coverage htmlcov/ # Distribution *.whl *.tar.gz ``` ### MANIFEST.in ``` # MANIFEST.in include README.md include LICENSE include pyproject.toml recursive-include src/my_package/data *.json recursive-include src/my_package/templates *.html recursive-exclude * __pycache__ recursive-exclude * *.py[co] ``` ## Checklist for Publishing - [ ] Code is tested (pytest passing) - [ ] Documentation is complete (README, docstrings) - [ ] Version number updated - [ ] CHANGELOG.md updated - [ ] License file included - [ ] pyproject.toml is complete - [ ] Package builds without errors - [ ] Installation tested in clean environment - [ ] CLI tools work (if applicable) - [ ] PyPI metadata is correct (classifiers, keywords) - [ ] GitHub repository linked - [ ] Tested on TestPyPI first - [ ] Git tag created for release ## Resources - **Python Packaging Guide**: https://packaging.python.org/ - **PyPI**: https://pypi.org/ - **TestPyPI**: https://test.pypi.org/ - **setuptools documentation**: https://setuptools.pypa.io/ - **build**: https://pypa-build.readthedocs.io/ - **twine**: https://twine.readthedocs.io/ ## Best Practices Summary 1. **Use src/ layout** for cleaner package structure 2. **Use pyproject.toml** for modern packaging 3. **Pin build dependencies** in build-system.requires 4. **Version appropriately** with semantic versioning 5. **Include all metadata** (classifiers, URLs, etc.) 6. **Test installation** in clean environments 7. **Use TestPyPI** before publishing to PyPI 8. **Document thoroughly** with README and docstrings 9. **Include LICENSE** file 10. **Automate publishing** with CI/CD