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

370 lines
9.8 KiB
Markdown

---
name: python-package-writer
description: 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`:
```python
"""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:
```toml
[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:
```python
# 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:
```python
# 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+):
```python
# 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)
```python
# 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:
```python
# 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:
- **[references/package-structure.md](./references/package-structure.md)** - Directory layouts, module organization
- **[references/pyproject-config.md](./references/pyproject-config.md)** - Complete pyproject.toml examples
- **[references/testing-patterns.md](./references/testing-patterns.md)** - pytest patterns, fixtures, CI setup
- **[references/type-hints.md](./references/type-hints.md)** - Modern typing patterns
- **[references/fastapi-integration.md](./references/fastapi-integration.md)** - FastAPI/Pydantic integration
- **[references/publishing.md](./references/publishing.md)** - PyPI publishing, CI/CD
- **[references/resources.md](./references/resources.md)** - Links to exemplary Python packages