--- 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