Files
claude-engineering-plugin/plugins/compound-engineering/skills/python-package-writer/SKILL.md
John Lamb fedf2ff8e4
Some checks failed
CI / test (push) Has been cancelled
rewrite ruby to python
2026-01-26 14:39:43 -06:00

9.8 KiB

name, description
name description
python-package-writer This skill should be used when writing Python packages following production-ready patterns and philosophy. It applies when creating new Python packages, refactoring existing packages, designing package APIs, or when clean, minimal, well-tested Python library code is needed. Triggers on requests like "create a package", "write a Python library", "design a package API", or mentions of PyPI publishing.

Python Package Writer

Write Python packages following battle-tested patterns from production-ready libraries. Emphasis on simplicity, minimal dependencies, comprehensive testing, and modern packaging standards (pyproject.toml, type hints, pytest).

Core Philosophy

Simplicity over cleverness. Zero or minimal dependencies. Explicit code over magic. Framework integration without framework coupling. Every pattern serves production use cases.

Package Structure (src layout)

The modern recommended layout with proper namespace isolation:

package-name/
├── pyproject.toml          # All metadata and configuration
├── README.md
├── LICENSE
├── py.typed                # PEP 561 marker for type hints
├── src/
│   └── package_name/       # Actual package code
│       ├── __init__.py     # Entry point, exports, version
│       ├── core.py         # Core functionality
│       ├── models.py       # Data models (Pydantic/dataclasses)
│       ├── exceptions.py   # Custom exceptions
│       └── py.typed        # Type hint marker (also here)
└── tests/
    ├── conftest.py         # Pytest fixtures
    ├── test_core.py
    └── test_models.py

Entry Point Structure

Every package follows this pattern in src/package_name/__init__.py:

"""Package description - one line."""

# Public API exports
from package_name.core import Client, process_data
from package_name.models import Config, Result
from package_name.exceptions import PackageError, ValidationError

__version__ = "1.0.0"
__all__ = [
    "Client",
    "process_data",
    "Config",
    "Result",
    "PackageError",
    "ValidationError",
]

pyproject.toml Configuration

Modern packaging with all metadata in one file:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "package-name"
version = "1.0.0"
description = "Brief description of what the package does"
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
authors = [
    { name = "Your Name", email = "you@example.com" }
]
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Typing :: Typed",
]
keywords = ["keyword1", "keyword2"]

# Zero or minimal runtime dependencies
dependencies = []

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "pytest-cov>=4.0",
    "ruff>=0.4",
    "mypy>=1.0",
]
# Optional integrations
fastapi = ["fastapi>=0.100", "pydantic>=2.0"]

[project.urls]
Homepage = "https://github.com/username/package-name"
Documentation = "https://package-name.readthedocs.io"
Repository = "https://github.com/username/package-name"
Changelog = "https://github.com/username/package-name/blob/main/CHANGELOG.md"

[tool.hatch.build.targets.wheel]
packages = ["src/package_name"]

[tool.ruff]
target-version = "py310"
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "C4", "SIM"]

[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_ignores = true

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra -q"

[tool.coverage.run]
source = ["src/package_name"]
branch = true

Configuration Pattern

Use module-level configuration with dataclasses or simple attributes:

# src/package_name/config.py
from dataclasses import dataclass, field
from os import environ
from typing import Any


@dataclass
class Config:
    """Package configuration with sensible defaults."""

    timeout: int = 30
    retries: int = 3
    api_key: str | None = field(default=None)
    debug: bool = False

    def __post_init__(self) -> None:
        # Environment variable fallbacks
        if self.api_key is None:
            self.api_key = environ.get("PACKAGE_API_KEY")


# Module-level singleton (optional)
_config: Config | None = None


def get_config() -> Config:
    """Get or create the global config instance."""
    global _config
    if _config is None:
        _config = Config()
    return _config


def configure(**kwargs: Any) -> Config:
    """Configure the package with custom settings."""
    global _config
    _config = Config(**kwargs)
    return _config

Error Handling

Simple hierarchy with informative messages:

# src/package_name/exceptions.py
class PackageError(Exception):
    """Base exception for all package errors."""
    pass


class ConfigError(PackageError):
    """Invalid configuration."""
    pass


class ValidationError(PackageError):
    """Data validation failed."""

    def __init__(self, message: str, field: str | None = None) -> None:
        self.field = field
        super().__init__(message)


class APIError(PackageError):
    """External API error."""

    def __init__(self, message: str, status_code: int | None = None) -> None:
        self.status_code = status_code
        super().__init__(message)


# Validate early with ValueError
def process(data: bytes) -> str:
    if not data:
        raise ValueError("Data cannot be empty")
    if len(data) > 1_000_000:
        raise ValueError(f"Data too large: {len(data)} bytes (max 1MB)")
    return data.decode("utf-8")

Type Hints

Always use type hints with modern syntax (Python 3.10+):

# Use built-in generics, not typing module
from collections.abc import Callable, Iterator, Mapping, Sequence

def process_items(
    items: list[str],
    transform: Callable[[str], str] | None = None,
    *,
    batch_size: int = 100,
) -> Iterator[str]:
    """Process items with optional transformation."""
    for item in items:
        if transform:
            yield transform(item)
        else:
            yield item


# Use | for unions, not Union
def get_value(key: str) -> str | None:
    return _cache.get(key)


# Use Self for return type annotations (Python 3.11+)
from typing import Self

class Client:
    def configure(self, **kwargs: str) -> Self:
        # Update configuration
        return self

Testing (pytest)

# tests/conftest.py
import pytest
from package_name import Config, configure


@pytest.fixture
def config() -> Config:
    """Fresh config for each test."""
    return configure(timeout=5, debug=True)


@pytest.fixture
def sample_data() -> bytes:
    """Sample input data."""
    return b"test data content"


# tests/test_core.py
import pytest
from package_name import process_data, PackageError


class TestProcessData:
    """Tests for process_data function."""

    def test_basic_functionality(self, sample_data: bytes) -> None:
        result = process_data(sample_data)
        assert result == "test data content"

    def test_empty_input_raises_error(self) -> None:
        with pytest.raises(ValueError, match="cannot be empty"):
            process_data(b"")

    def test_with_transform(self, sample_data: bytes) -> None:
        result = process_data(sample_data, transform=str.upper)
        assert result == "TEST DATA CONTENT"


class TestConfig:
    """Tests for configuration."""

    def test_defaults(self) -> None:
        config = Config()
        assert config.timeout == 30
        assert config.retries == 3

    def test_env_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("PACKAGE_API_KEY", "test-key")
        config = Config()
        assert config.api_key == "test-key"

FastAPI Integration

Optional FastAPI integration pattern:

# src/package_name/fastapi.py
"""FastAPI integration - only import if FastAPI is installed."""
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from fastapi import FastAPI

from package_name.config import get_config


def init_app(app: "FastAPI") -> None:
    """Initialize package with FastAPI app."""
    config = get_config()

    @app.on_event("startup")
    async def startup() -> None:
        # Initialize connections, caches, etc.
        pass

    @app.on_event("shutdown")
    async def shutdown() -> None:
        # Cleanup resources
        pass


# Usage in FastAPI app:
# from package_name.fastapi import init_app
# init_app(app)

Anti-Patterns to Avoid

  • __getattr__ magic (use explicit imports)
  • Global mutable state (use configuration objects)
  • * imports in __init__.py (explicit __all__)
  • Many runtime dependencies
  • Committing .venv/ or __pycache__/
  • Not including py.typed marker
  • Using setup.py (use pyproject.toml)
  • Mixing src layout and flat layout
  • print() for debugging (use logging)
  • Bare except: clauses

Reference Files

For deeper patterns, see: