
Python Architect
- 10 installs
- 38 repo stars
- Updated August 4, 2026
- maxvaega/awesome-skills
Helps with python tasks.
About
python-architect is a Claude Code skill for python. It helps solo builders move faster with AI-assisted coding.
- python-architect
- Python
- AI-coding skill
Python Architect by the numbers
- 10 all-time installs (skills.sh)
- Ranked #207 of 290 Python skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/maxvaega/awesome-skills --skill python-architectAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 10 |
|---|---|
| repo stars | ★ 38 |
| Last updated | August 4, 2026 |
| Repository | maxvaega/awesome-skills ↗ |
What it does
Helps with python tasks.
Files
Python Library Architect
Overview
This skill enables the agent to function as a Senior Python Library Architect, guiding the design and development of robust, maintainable, scalable, and user-friendly Python code, specifically for python libraries. It combines architectural vision with practical implementation knowledge, considering long-term maintainability, backwards compatibility, and developer experience.
When to Use This Skill
Trigger this skill for:
- Design from scratch: "Help me architect a new Python library for..."
- Architectural decisions: "Should I use class-based or function-based design for..."
- Think as architect: "Think as an architect and review my code structure..."
- Code review: "Review my code for architectural issues..."
- Pattern guidance: "How should I structure X in my library?"
- API design: "What's the best API design for..."
- Testing strategy: "How should I organize tests for..."
- Troubleshooting: "My repo has a design problem with..."
Core Approach
When engaging with library architecture questions, adopt this response pattern:
1. Ask Clarifying Questions (if ambiguous):
- What is the library's primary purpose?
- Who are the target users?
- What are the key use cases?
- Any specific constraints (performance, dependencies, Python versions)?
2. Provide Multiple Options with trade-offs using the question tool:
- Option A: [Description] - Pros: [...] - Cons: [...]
- Option B: [Description] - Pros: [...] - Cons: [...]
- Recommendation: [Which and why]
3. Include Code Examples:
- Show concrete implementations
- Include type hints
- Add docstrings
- Demonstrate best practices
4. Explain Rationale:
- Why this approach?
- What problems does it solve?
- Alternatives and when to use them
- When to choose differently
5. Consider Full Lifecycle:
- How will this evolve?
- Version migration strategies
- Testing approach
- Documentation needs
Fundamental Architectural Principles
Reference references/architectural-principles.md for comprehensive guidance on:
- Package structure and organization (src/ layout)
- API design principles (Pythonic design, stability, configuration)
- SOLID principles application
- Error handling and exceptions
- Type annotations and static typing
- Documentation standards
- Testing strategy
- Versioning and backwards compatibility
- Dependency management
- Code quality and style
- Extensibility and plugin architecture
- Performance considerations
- Security considerations
Python Standards Reference
Reference references/pep-standards.md for quick guidance on:
- PEP 8: Style Guide for Python Code
- PEP 257: Docstring Conventions
- PEP 484: Type Hints
- PEP 517/518: Build System
- PEP 440: Version Identification
- PEP 621: Storing project metadata in pyproject.toml
- PEP 427/430: Wheels and distributions
Project Templates and Examples
Use bundled assets for quick-start templates:
assets/pyproject.toml.template- Production-ready pyproject.toml structureassets/README.md.template- Comprehensive README templateassets/project-structure.txt- Recommended package organizationassets/CONTRIBUTING.md.template- Contribution guide templateassets/test-structure.txt- Recommended test organizationassets/example-exceptions.py- Custom exception hierarchy patternassets/example-config.py- Configuration pattern example
Common Architectural Scenarios
Scenario 1: Designing a New Library
Process: 1. Understand the problem domain and users 2. Design the public API first (API-driven design) 3. Plan package structure using src/ layout 4. Define custom exception hierarchy 5. Plan testing strategy 6. Design extension points if needed
Reference architectural principles and use templates to scaffold the project structure.
Scenario 2: Reviewing Existing Library Code
Evaluation checklist:
- [ ] Uses src/ layout properly
- [ ] Public API clearly defined in
__init__.py - [ ] Type hints on all public APIs
- [ ] Comprehensive docstrings (Google or NumPy style)
- [ ] Custom exception hierarchy defined
- [ ] >90% test coverage for public APIs
- [ ] No breaking changes in minor versions
- [ ] Clear deprecation path for removed features
- [ ] Dependencies justified and minimal
- [ ] Code follows PEP 8 (Black, Ruff, etc.)
Scenario 3: Architectural Problem-Solving
When facing design challenges: 1. Identify the core problem (tight coupling, poor API, etc.) 2. Reference relevant principles (SOLID, DIP, OCP) 3. Propose multiple solutions with trade-offs 4. Recommend best fit for their constraints 5. Provide implementation guidance
Scenario 4: API Design Decisions
Key considerations:
- Design for
import libthenlib.Thing()pattern - Use short, clear names
- Support duck typing where possible
- Prefer keyword arguments
- Expose only public API in
__init__.py - Mark internal APIs with
_leading_underscore - Define
__all__explicitly
Tools and Ecosystem
Recommended tools for Python library development:
- Build: hatchling, setuptools, poetry, flit
- Testing: pytest, hypothesis, tox
- Type Checking: mypy (strict mode), pyright, pyre
- Linting/Formatting: ruff, black, flake8, pylint
- Documentation: sphinx, mkdocs, pdoc
- CI/CD: GitHub Actions, GitLab CI, Azure Pipelines
When to Push Back
Respectfully challenge decisions that:
- Break backwards compatibility without major version bump
- Introduce unnecessary complexity
- Violate Python conventions without good reason
- Create security vulnerabilities
- Make the library difficult to test
- Lock users into specific implementations
Always explain why and suggest alternatives.
Output Format
Structure responses as:
1. Brief Summary: 1-2 sentence direct answer 2. Recommended Approach: Detailed explanation with code 3. Trade-offs: What you gain and lose with this approach 4. Alternatives: Other valid approaches and when to use them 5. Implementation Steps: Concrete action items 6. Testing Strategy: How to verify the implementation 7. Documentation Needs: What to document for users
Be:
- Precise: Give specific, actionable guidance
- Practical: Focus on real-world applicability
- Thorough: Consider edge cases and long-term implications
- Pythonic: Embrace Python idioms and conventions
- Thoughtful: Explain your reasoning and trade-offs
Goal
Help create Python libraries that are:
- Reliable: Well-tested, handles errors gracefully
- Maintainable: Clean code, good documentation, follows conventions
- Extensible: Can grow and adapt to new requirements
- User-Friendly: Intuitive API, helpful errors, great documentation
- Production-Ready: Secure, performant, stable
# Contributing to MyPackage
Thank you for your interest in contributing to MyPackage! This document provides guidelines and instructions for getting involved.
## Code of Conduct
This project adheres to the Contributor Covenant [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [maintainer-email@example.com].
## Getting Started
### Development Setup
1. **Clone the repository**
```bash
git clone https://github.com/username/mypackage.git
cd mypackage
```
2. **Create a virtual environment**
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. **Install development dependencies**
```bash
pip install -e ".[dev]"
```
4. **Install pre-commit hooks**
```bash
pre-commit install
```
### Testing Your Setup
Run the test suite to verify everything works:
```bash
pytest
```
## How to Contribute
### Report Bugs
**Before creating a bug report:**
- Check existing issues to avoid duplicates
- Verify the issue reproduces on the latest main branch
**When creating a bug report, include:**
- Clear, descriptive title
- Exact steps to reproduce
- Expected vs actual behavior
- Python version and OS
- Relevant code snippets
- Full error traceback if applicable
### Suggest Enhancements
**Before requesting a feature:**
- Check existing issues and discussions
- Ensure it aligns with the library's scope
**When suggesting an enhancement:**
- Clear use case and motivation
- Example of how you'd use the feature
- Possible drawbacks or alternatives
### Code Contributions
We follow a standard fork-and-pull-request workflow:
1. **Fork the repository** on GitHub
2. **Create a feature branch**
```bash
git checkout -b feature/your-feature-name
```
3. **Make your changes**
- Follow the code style guidelines below
- Add tests for new functionality
- Update documentation as needed
4. **Run checks locally**
```bash
# Format code
black src/ tests/
# Lint code
ruff check --fix src/ tests/
# Type check
mypy src/
# Run tests
pytest
# Check coverage
pytest --cov=src/mypackage
```
5. **Commit with clear messages**
```bash
git commit -m "feat: Add new feature"
git commit -m "fix: Resolve bug in validator"
```
Use conventional commits: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`, `chore:`
6. **Push and create a pull request**
```bash
git push origin feature/your-feature-name
```
Open a PR on GitHub with a clear description
## Code Style Guidelines
### Python Style
We use the following tools to maintain consistent code style:
- **Black** for code formatting (88-character line length)
- **Ruff** for linting
- **mypy** for type checking (strict mode)
- **isort** for import sorting
### Type Hints
All public APIs **must** have type hints:
```python
from typing import Optional
from collections.abc import Sequence
def process_items(
items: Sequence[str],
limit: Optional[int] = None
) -> dict[str, int]:
"""Process items and return frequency map.
Args:
items: Items to process
limit: Optional limit on items to process
Returns:
Frequency map of items
"""
...
```
### Docstrings
Use Google-style docstrings for all public functions and classes:
```python
def calculate(x: int, y: int) -> int:
"""Calculate sum of two numbers.
Args:
x: First number
y: Second number
Returns:
Sum of x and y
Raises:
TypeError: If inputs are not integers
Example:
>>> calculate(2, 3)
5
"""
...
```
### Naming Conventions
- **Functions/variables**: `lowercase_with_underscores`
- **Classes**: `PascalCase`
- **Constants**: `UPPER_CASE_WITH_UNDERSCORES`
- **Private/internal**: `_leading_underscore`
## Testing Guidelines
### Writing Tests
Tests go in the `tests/` directory and use pytest:
```python
import pytest
from mypackage.core import process
def test_process_basic():
"""Test basic processing."""
result = process("input")
assert result == "expected_output"
@pytest.mark.parametrize("input,expected", [
("a", "A"),
("b", "B"),
])
def test_process_varied(input, expected):
"""Test with varied inputs."""
assert process(input) == expected
def test_process_error():
"""Test error handling."""
with pytest.raises(ValueError):
process(None)
```
### Coverage Requirements
- Aim for >90% code coverage on public APIs
- All error paths should be tested
- Both success and failure cases
Run tests with coverage:
```bash
pytest --cov=src/mypackage --cov-report=html
```
## Documentation
### Updating Documentation
Documentation lives in `docs/source/`:
1. **API Documentation**: Generated from docstrings via Sphinx
- Keep docstrings up-to-date with code changes
- Use type hints and examples
2. **User Guides**: In `docs/source/guide/`
- Explain concepts with examples
- Show common use cases
3. **README.md**:
- Keep quick-start examples current
- Update feature list
### Building Documentation Locally
```bash
cd docs
make html
# View at docs/build/html/index.html
```
## Commit Message Conventions
Use [Conventional Commits](https://www.conventionalcommits.org/):
```
<type>(<scope>): <subject>
<body>
<footer>
```
Types:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation
- `style`: Code style (formatting, etc.)
- `refactor`: Code refactoring
- `test`: Adding/updating tests
- `chore`: Build, dependencies, etc.
Example:
```
feat(processor): Add batch processing support
Implements parallel processing for large datasets.
Improves performance by 3x for typical use cases.
Closes #42
```
## Pull Request Process
1. **Update CHANGELOG.md** with your changes
2. **Ensure tests pass** and coverage is maintained
3. **Get review approval** from maintainers
4. **Squash commits** if requested (keep history clean)
5. **PR gets merged** once approved
### PR Guidelines
- **One feature per PR** (unless closely related)
- **Descriptive title and description**
- **Reference issues** with "Closes #123" or "Fixes #123"
- **Keep scope focused** (avoid mixing refactoring with features)
## Release Process
Maintainers handle releases following semantic versioning:
- **MAJOR**: Breaking changes
- **MINOR**: New features (backwards compatible)
- **PATCH**: Bug fixes (backwards compatible)
Releases are tagged, published to PyPI, and documented in CHANGELOG.md.
## Asking for Help
- **Questions about code**: Open a discussion on GitHub
- **Design feedback**: Comment on relevant issues
- **Need guidance**: Check documentation or ask maintainers
- **Found a problem**: Open an issue with details
## Recognition
Contributors are recognized in:
- CHANGELOG.md (for significant contributions)
- GitHub contributors page
- Release notes
Thank you for contributing! 🎉
"""Example configuration patterns for Python libraries.
This file demonstrates best practices for handling configuration
in production-grade Python libraries.
"""
import os
from dataclasses import dataclass, field
from typing import Optional
from pathlib import Path
# Pattern 1: Dataclass-based Configuration
# ==========================================
@dataclass
class DatabaseConfig:
"""Database connection configuration.
Attributes:
host: Database host address
port: Database port
username: Database username
password: Database password (use env var, never in code)
database: Database name
timeout: Connection timeout in seconds
pool_size: Connection pool size
ssl: Whether to use SSL for connection
"""
host: str = "localhost"
port: int = 5432
username: str = "user"
password: str = "" # Set from environment
database: str = "mydb"
timeout: int = 30
pool_size: int = 10
ssl: bool = True
def __post_init__(self):
"""Validate configuration after initialization."""
if self.port < 1 or self.port > 65535:
raise ValueError(f"Invalid port number: {self.port}")
if self.timeout <= 0:
raise ValueError(f"Timeout must be positive, got {self.timeout}")
if self.pool_size < 1:
raise ValueError(f"Pool size must be positive, got {self.pool_size}")
@dataclass
class LibraryConfig:
"""Main library configuration.
Attributes:
debug: Enable debug mode
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
cache_enabled: Whether to enable caching
cache_ttl: Cache time-to-live in seconds
database: Database configuration
timeout: Default operation timeout in seconds
max_retries: Maximum number of retries for operations
retry_backoff: Exponential backoff multiplier for retries
"""
debug: bool = False
log_level: str = "INFO"
cache_enabled: bool = True
cache_ttl: int = 3600 # 1 hour
database: DatabaseConfig = field(default_factory=DatabaseConfig)
timeout: int = 30
max_retries: int = 3
retry_backoff: float = 2.0
@classmethod
def from_env(cls) -> "LibraryConfig":
"""Create configuration from environment variables.
Environment variables:
MYLIB_DEBUG: Set to "1", "true", "yes" for debug mode
MYLIB_LOG_LEVEL: Logging level (default: INFO)
MYLIB_CACHE_ENABLED: Set to "0", "false", "no" to disable cache
MYLIB_CACHE_TTL: Cache TTL in seconds (default: 3600)
MYLIB_DB_HOST: Database host (default: localhost)
MYLIB_DB_PORT: Database port (default: 5432)
MYLIB_DB_USER: Database username (default: user)
MYLIB_DB_PASSWORD: Database password (required if used)
MYLIB_DB_NAME: Database name (default: mydb)
MYLIB_TIMEOUT: Operation timeout (default: 30)
MYLIB_MAX_RETRIES: Max retries (default: 3)
"""
return cls(
debug=os.getenv("MYLIB_DEBUG", "").lower() in ("1", "true", "yes"),
log_level=os.getenv("MYLIB_LOG_LEVEL", "INFO"),
cache_enabled=os.getenv("MYLIB_CACHE_ENABLED", "1").lower() not in ("0", "false", "no"),
cache_ttl=int(os.getenv("MYLIB_CACHE_TTL", "3600")),
database=DatabaseConfig(
host=os.getenv("MYLIB_DB_HOST", "localhost"),
port=int(os.getenv("MYLIB_DB_PORT", "5432")),
username=os.getenv("MYLIB_DB_USER", "user"),
password=os.getenv("MYLIB_DB_PASSWORD", ""),
database=os.getenv("MYLIB_DB_NAME", "mydb"),
),
timeout=int(os.getenv("MYLIB_TIMEOUT", "30")),
max_retries=int(os.getenv("MYLIB_MAX_RETRIES", "3")),
)
@classmethod
def from_file(cls, config_path: str) -> "LibraryConfig":
"""Load configuration from a YAML or JSON file.
Args:
config_path: Path to configuration file
Returns:
Loaded configuration
Raises:
FileNotFoundError: If config file doesn't exist
ValueError: If config file format is invalid
"""
import json
from pathlib import Path
path = Path(config_path)
if not path.exists():
raise FileNotFoundError(f"Configuration file not found: {config_path}")
if path.suffix == ".json":
with open(path) as f:
data = json.load(f)
elif path.suffix in (".yaml", ".yml"):
try:
import yaml
except ImportError:
raise ImportError("PyYAML required for YAML config. Install with: pip install pyyaml")
with open(path) as f:
data = yaml.safe_load(f)
else:
raise ValueError(f"Unsupported config format: {path.suffix}")
# Convert dict to config object
return cls(**data)
# Pattern 2: Builder Pattern for Complex Configuration
# ======================================================
class ConfigBuilder:
"""Builder for constructing complex configurations.
Allows gradual configuration with sensible defaults:
config = (ConfigBuilder()
.with_debug(True)
.with_database("postgresql://localhost/mydb")
.with_cache_ttl(7200)
.build())
"""
def __init__(self):
"""Initialize builder with defaults."""
self._debug = False
self._log_level = "INFO"
self._cache_enabled = True
self._cache_ttl = 3600
self._db_config = DatabaseConfig()
self._timeout = 30
self._max_retries = 3
def with_debug(self, debug: bool) -> "ConfigBuilder":
"""Enable/disable debug mode."""
self._debug = debug
return self
def with_log_level(self, level: str) -> "ConfigBuilder":
"""Set logging level."""
if level not in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
raise ValueError(f"Invalid log level: {level}")
self._log_level = level
return self
def with_database(self, host: str, port: int = 5432, **kwargs) -> "ConfigBuilder":
"""Configure database connection."""
self._db_config = DatabaseConfig(
host=host,
port=port,
**kwargs
)
return self
def with_cache(self, enabled: bool = True, ttl: int = 3600) -> "ConfigBuilder":
"""Configure caching."""
self._cache_enabled = enabled
self._cache_ttl = ttl
return self
def with_timeout(self, timeout: int) -> "ConfigBuilder":
"""Set operation timeout."""
if timeout <= 0:
raise ValueError(f"Timeout must be positive, got {timeout}")
self._timeout = timeout
return self
def build(self) -> LibraryConfig:
"""Build the final configuration."""
return LibraryConfig(
debug=self._debug,
log_level=self._log_level,
cache_enabled=self._cache_enabled,
cache_ttl=self._cache_ttl,
database=self._db_config,
timeout=self._timeout,
)
# Pattern 3: Configuration with Optional Dependencies
# ====================================================
@dataclass
class OptionalFeaturesConfig:
"""Configuration for optional features.
Only load dependencies if features are actually used.
"""
enable_caching: bool = True
cache_backend: str = "memory" # "memory", "redis"
enable_compression: bool = False
compression_level: int = 6
def get_cache_backend(self):
"""Lazy-load cache backend only if needed."""
if not self.enable_caching:
return None
if self.cache_backend == "memory":
return MemoryCache()
elif self.cache_backend == "redis":
try:
import redis
except ImportError:
raise ImportError(
"Redis cache backend requires 'redis' package. "
"Install with: pip install redis"
)
return RedisCache()
else:
raise ValueError(f"Unknown cache backend: {self.cache_backend}")
class MemoryCache:
"""Simple in-memory cache."""
pass
class RedisCache:
"""Redis-backed cache."""
pass
# Pattern 4: Validation and Defaults
# ===================================
@dataclass
class APIConfig:
"""API configuration with validation.
Demonstrates setting sensible defaults and validating
configuration values.
"""
api_key: str
api_secret: str
base_url: str = "https://api.example.com"
timeout: int = 30
max_connections: int = 100
verify_ssl: bool = True
def __post_init__(self):
"""Validate configuration after initialization."""
if not self.api_key:
raise ValueError("api_key is required and cannot be empty")
if not self.api_secret:
raise ValueError("api_secret is required and cannot be empty")
if not self.base_url:
raise ValueError("base_url is required and cannot be empty")
if self.timeout <= 0:
raise ValueError(f"timeout must be positive, got {self.timeout}")
if self.max_connections < 1:
raise ValueError(f"max_connections must be at least 1, got {self.max_connections}")
@classmethod
def from_env(cls) -> "APIConfig":
"""Load API configuration from environment variables.
Environment variables:
MYLIB_API_KEY (required)
MYLIB_API_SECRET (required)
MYLIB_API_URL (optional, default: https://api.example.com)
MYLIB_API_TIMEOUT (optional, default: 30)
MYLIB_API_VERIFY_SSL (optional, default: true)
"""
api_key = os.getenv("MYLIB_API_KEY")
if not api_key:
raise ValueError(
"MYLIB_API_KEY environment variable is required. "
"Set it with: export MYLIB_API_KEY=your_key"
)
api_secret = os.getenv("MYLIB_API_SECRET")
if not api_secret:
raise ValueError(
"MYLIB_API_SECRET environment variable is required. "
"Set it with: export MYLIB_API_SECRET=your_secret"
)
return cls(
api_key=api_key,
api_secret=api_secret,
base_url=os.getenv("MYLIB_API_URL", "https://api.example.com"),
timeout=int(os.getenv("MYLIB_API_TIMEOUT", "30")),
verify_ssl=os.getenv("MYLIB_API_VERIFY_SSL", "true").lower() in ("1", "true", "yes"),
)
# Usage Examples
# ==============
def example_dataclass_config():
"""Example: Create configuration with dataclass."""
config = LibraryConfig(
debug=True,
log_level="DEBUG",
database=DatabaseConfig(
host="db.example.com",
username="admin",
password=os.getenv("DB_PASSWORD"),
),
timeout=60,
)
print(f"Config: {config}")
def example_from_env():
"""Example: Load configuration from environment variables."""
# Set environment variables first:
# export MYLIB_DEBUG=1
# export MYLIB_LOG_LEVEL=DEBUG
# export MYLIB_DB_HOST=db.example.com
config = LibraryConfig.from_env()
print(f"Debug mode: {config.debug}")
print(f"Database host: {config.database.host}")
def example_from_file():
"""Example: Load configuration from file."""
# config.json:
# {
# "debug": true,
# "log_level": "DEBUG",
# "database": {
# "host": "db.example.com",
# "password": "secret"
# }
# }
config = LibraryConfig.from_file("config.json")
print(f"Config loaded from file: {config}")
def example_builder():
"""Example: Use builder pattern for fluent configuration."""
config = (ConfigBuilder()
.with_debug(True)
.with_log_level("DEBUG")
.with_database("db.example.com", port=5432, username="admin")
.with_cache(enabled=True, ttl=7200)
.with_timeout(60)
.build())
print(f"Config built with builder: {config}")
# Best Practices Summary
# ======================
"""
1. USE DATACLASSES FOR CONFIGURATION
- Cleaner than writing __init__ manually
- Type hints provide IDE support
- Easy serialization/deserialization
2. PROVIDE MULTIPLE LOADING METHODS
- from_env(): Load from environment variables
- from_file(): Load from config files
- Direct instantiation: Simple cases
3. VALIDATE IN __post_init__
- Check required values
- Validate ranges and constraints
- Provide helpful error messages
4. USE SENSIBLE DEFAULTS
- Default values prevent required arguments
- Documents expected configuration
- Simplifies common use cases
5. BUILDER PATTERN FOR COMPLEX CONFIGS
- Fluent interface for readability
- Gradual construction
- Works well with optional features
6. NEVER HARDCODE CREDENTIALS
- Use environment variables
- Use config files (not in repo)
- Document required env variables
7. LAZY LOAD OPTIONAL DEPENDENCIES
- Only import when feature is used
- Provide helpful error messages
- Keep core library lightweight
8. CONSISTENT NAMING
- Environment variables: MYLIB_FEATURE_SETTING
- Config file keys: feature.setting
- Python attributes: feature_setting (snake_case)
"""
"""Example custom exception hierarchy for a Python library.
This file demonstrates best practices for defining exception hierarchies
in production-grade Python libraries.
"""
class MyLibraryError(Exception):
"""Base exception for all mylib errors.
All exceptions raised by the library inherit from this class,
allowing users to catch all library-specific errors with:
try:
...
except MyLibraryError as e:
...
"""
pass
# Configuration-related errors
class ConfigurationError(MyLibraryError):
"""Raised when configuration is invalid.
Example:
raise ConfigurationError(
f"Missing required config key 'database_url'. "
f"Provide via config parameter or DATABASE_URL env variable."
)
"""
pass
class MissingConfigError(ConfigurationError):
"""Raised when required configuration is missing."""
pass
class InvalidConfigError(ConfigurationError):
"""Raised when configuration values are invalid."""
pass
# Validation-related errors
class ValidationError(MyLibraryError):
"""Raised when input validation fails.
Example:
if not isinstance(age, int):
raise ValidationError(
f"Parameter 'age' must be an integer, got {type(age).__name__}"
)
"""
pass
class InputError(ValidationError):
"""Raised when user input is invalid."""
pass
class DataFormatError(ValidationError):
"""Raised when data format is not as expected."""
pass
# API/Communication errors
class APIError(MyLibraryError):
"""Raised when API operations fail.
Example:
try:
response = requests.get(url)
response.raise_for_status()
except requests.RequestException as e:
raise APIError(
f"Failed to fetch from {url}: {str(e)}. "
f"Check your network connection and URL."
) from e
"""
pass
class NetworkError(APIError):
"""Raised when network operations fail."""
pass
class TimeoutError(APIError):
"""Raised when operation times out.
Note: This shadows the built-in TimeoutError. Consider using
a different name like `OperationTimeout` if needed.
"""
pass
# Resource-related errors
class ResourceError(MyLibraryError):
"""Raised when resource operations fail."""
pass
class ResourceNotFoundError(ResourceError):
"""Raised when a required resource is not found.
Example:
if not os.path.exists(filepath):
raise ResourceNotFoundError(
f"Configuration file not found: {filepath}. "
f"Create a config file at {default_path} or "
f"pass 'config_path' parameter."
)
"""
pass
class PermissionError(ResourceError):
"""Raised when operation is not permitted.
Note: This shadows the built-in PermissionError. Consider using
a different name like `AccessDenied` if needed.
"""
pass
class InsufficientResourcesError(ResourceError):
"""Raised when insufficient resources are available."""
pass
# State-related errors
class StateError(MyLibraryError):
"""Raised when operation is invalid for current state.
Example:
if not self.is_initialized:
raise StateError(
"Client not initialized. Call client.initialize() first."
)
"""
pass
class NotInitializedError(StateError):
"""Raised when required initialization hasn't been performed."""
pass
class AlreadyExistsError(StateError):
"""Raised when trying to create something that already exists."""
pass
# Implementation errors
class NotImplementedError(MyLibraryError):
"""Raised when feature is not yet implemented."""
pass
class UnsupportedOperationError(MyLibraryError):
"""Raised when operation is not supported.
Example:
if not hasattr(processor, 'process'):
raise UnsupportedOperationError(
f"Processor {type(processor).__name__} doesn't support "
f"process operation. Use a different processor."
)
"""
pass
# Usage Examples
# ==============
def example_validation_error():
"""Example of raising a validation error with helpful message."""
timeout = -5
if timeout < 0:
raise ValidationError(
f"Parameter 'timeout' must be non-negative, got {timeout}. "
f"Use timeout=30 for a 30-second timeout."
)
def example_chained_exception():
"""Example of chaining exceptions to preserve context."""
import requests
try:
response = requests.get("https://example.com", timeout=5)
response.raise_for_status()
except requests.RequestException as e:
raise APIError(
f"Failed to fetch data from API: {str(e)}. "
f"Check your network connection and API endpoint."
) from e
def example_missing_resource():
"""Example of reporting missing resources clearly."""
import os
config_path = "/etc/myapp/config.json"
if not os.path.exists(config_path):
raise ResourceNotFoundError(
f"Configuration file not found: {config_path}\n"
f"Create a config file or set MYAPP_CONFIG_PATH environment variable.\n"
f"See documentation at: https://docs.example.com/config"
)
def example_state_error():
"""Example of reporting state-related errors."""
class Client:
def __init__(self):
self._connected = False
def query(self, sql):
if not self._connected:
raise StateError(
"Client not connected. Call client.connect() before "
"executing queries."
)
# Execute query...
client = Client()
try:
client.query("SELECT * FROM users")
except StateError as e:
print(f"Connection error: {e}")
# Best Practices Summary
# ======================
"""
1. DEFINE A BASE EXCEPTION
- All library exceptions inherit from it
- Users can catch all library errors with: except MyLibraryError
2. CREATE SEMANTIC SUBCLASSES
- Group related errors (ConfigurationError, ValidationError, etc.)
- Allows specific error handling
3. WRITE HELPFUL ERROR MESSAGES
- State what was wrong
- Explain why it's wrong
- Show how to fix it
- Example: f"X must be Y, got {Z}. Use X=default for default behavior."
4. CHAIN EXCEPTIONS WHEN WRAPPING
- Use `raise ... from e` to preserve context
- Helps with debugging and tracing
5. DOCUMENT EXCEPTIONS
- Add docstrings to exception classes
- Include examples in docstrings
- Document which operations raise which exceptions
6. AVOID SHADOWING BUILT-INS
- Consider naming like TimeoutError -> OperationTimeout
- Be careful with common exception names
7. USE EXCEPTIONS FOR EXCEPTIONAL CASES
- Not for normal control flow
- Return None or empty collection for "not found" when appropriate
- Reserve exceptions for actual errors
8. CONSISTENCY
- Same error for same condition across codebase
- Consistent error message format
- Clear exception hierarchy
"""
RECOMMENDED PROJECT STRUCTURE (src/ layout)
==========================================
mypackage/ # Repository root
│
├── .github/ # GitHub specific
│ ├── workflows/
│ │ ├── ci.yml # CI/CD pipeline
│ │ └── release.yml # Release automation
│ └── ISSUE_TEMPLATE/
│
├── src/ # Source code (src/ layout required!)
│ └── mypackage/ # Main package
│ ├── __init__.py # Public API exports
│ ├── __version__.py # Version string
│ ├── exceptions.py # Custom exception hierarchy
│ ├── core/ # Core functionality
│ │ ├── __init__.py
│ │ ├── processor.py
│ │ └── validator.py
│ ├── utils/ # Utility functions
│ │ ├── __init__.py
│ │ ├── helpers.py
│ │ └── decorators.py
│ ├── config.py # Configuration classes
│ └── _internal/ # Private/internal modules
│ ├── __init__.py
│ └── backend.py
│
├── tests/ # Test suite
│ ├── __init__.py
│ ├── conftest.py # pytest fixtures and config
│ ├── unit/ # Unit tests (test individual components)
│ │ ├── test_core.py
│ │ ├── test_validator.py
│ │ └── test_config.py
│ ├── integration/ # Integration tests (test workflows)
│ │ ├── test_workflow_a.py
│ │ └── test_workflow_b.py
│ └── fixtures/ # Test data and fixtures
│ └── sample_data.json
│
├── docs/ # Documentation
│ ├── source/
│ │ ├── conf.py # Sphinx config
│ │ ├── index.rst
│ │ ├── guide/
│ │ │ ├── installation.rst
│ │ │ ├── quickstart.rst
│ │ │ └── advanced.rst
│ │ ├── api/
│ │ │ └── index.rst # API reference (auto-generated from docstrings)
│ │ └── changelog.rst
│ ├── build/ # Built docs (generated)
│ └── Makefile # Documentation build
│
├── scripts/ # Development scripts
│ ├── check_types.sh
│ ├── run_tests.sh
│ └── build_release.sh
│
├── .gitignore # Git ignore rules
├── .gitattributes # Git attributes
├── .pre-commit-config.yaml # Pre-commit hooks
│
├── pyproject.toml # Project metadata & build config (PEP 517/518)
├── README.md # Project overview
├── CHANGELOG.md # Version history
├── CONTRIBUTING.md # Contributing guidelines
├── CODE_OF_CONDUCT.md # Community guidelines
├── LICENSE # License (MIT, Apache, etc.)
├── SECURITY.md # Security policy
│
├── Makefile # Development convenience commands
└── tox.ini # Test automation config
KEY STRUCTURE DECISIONS
=======================
1. SRC/ LAYOUT (REQUIRED)
- src/ prevents accidentally importing from source during tests
- Ensures you always test the installed version
- Industry standard for modern Python packages
2. INTERNAL UNDERSCORE CONVENTION
- Public modules: no leading underscore (src/mypackage/core.py)
- Private modules: leading underscore (src/mypackage/_internal.py)
- Signals to users: this is implementation detail, don't depend on it
3. FLAT HIERARCHY
- Avoid deep nesting (keep 2-3 levels maximum)
- Package structure is implementation detail
- Expose public API at package level via __init__.py
4. SEPARATION OF CONCERNS
- core/: Main algorithm/logic
- utils/: Helper functions and utilities
- config.py: Configuration classes
- exceptions.py: Custom exception hierarchy
- _internal/: Implementation details
5. TEST STRUCTURE
- unit/: Test individual components (fast, no I/O)
- integration/: Test workflows (may be slower)
- conftest.py: Shared fixtures and setup
- fixtures/: Sample data needed for tests
6. DOCUMENTATION
- README.md: Quick start and overview
- docs/source/: Full documentation (Sphinx)
- Docstrings: API documentation in code
- CONTRIBUTING.md: How to contribute
- CHANGELOG.md: Version history
PUBLIC API DESIGN
=================
In src/mypackage/__init__.py:
```python
"""MyPackage - [One-line description]."""
from mypackage._version import __version__
from mypackage.exceptions import MyPackageError, ValidationError
from mypackage.core.processor import Processor
from mypackage.core.validator import validate
from mypackage.config import Config
__all__ = [
"__version__",
"Config",
"Processor",
"validate",
"MyPackageError",
"ValidationError",
]
```
Users import like:
- `from mypackage import Processor`
- `import mypackage`
- `mypackage.Processor()`
NOT:
- `from mypackage.core.processor import Processor` (internal detail)
- `from mypackage._internal import Backend` (private)
MODULE ORGANIZATION PATTERNS
============================
Pattern 1: Function-based core
- core/
├── __init__.py (exports main functions)
├── processor.py (main algorithm)
├── validator.py (validation logic)
└── transformer.py (transformation logic)
Pattern 2: Class-based core with strategy pattern
- core/
├── __init__.py (exports main classes)
├── base.py (abstract classes)
├── default.py (default implementation)
└── specialized.py (specialized implementations)
Pattern 3: Large library with submodules
- core/
- processing/
- __init__.py
- (modules)
- validation/
- __init__.py
- (modules)
- storage/
- __init__.py
- (modules)
All expose high-level API at package level!
SIZE GUIDELINES
===============
Small library (< 100 functions):
- src/mypackage/
├── __init__.py
├── core.py (main logic)
└── utils.py (helpers)
Medium library (100-500 functions):
- src/mypackage/
├── __init__.py
├── core/
├── utils/
└── cli/ (optional)
Large library (500+ functions):
- src/mypackage/
├── __init__.py
├── core/
├── processing/
├── storage/
├── utils/
└── _internal/
Don't over-organize! Flat is better than nested.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mypackage"
version = "1.0.0"
description = "A brief, one-sentence description of your library"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "your@email.com"}
]
maintainers = [
{name = "Your Name", email = "your@email.com"}
]
keywords = ["keyword1", "keyword2"]
classifiers = [
"Development Status :: 4 - Beta",
"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",
]
dependencies = [
# Add runtime dependencies with semantic version constraints
# "requests>=2.28,<3.0",
# "pydantic>=2.0,<3.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"black>=22.0",
"mypy>=1.0",
"ruff>=0.1.0",
"isort>=5.0",
]
docs = [
"sphinx>=6.0",
"sphinx-rtd-theme>=1.0",
"sphinx-autodoc-typehints>=1.0",
]
[project.urls]
Homepage = "https://github.com/username/mypackage"
Documentation = "https://mypackage.readthedocs.io"
Repository = "https://github.com/username/mypackage.git"
Issues = "https://github.com/username/mypackage/issues"
Changelog = "https://github.com/username/mypackage/blob/main/CHANGELOG.md"
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "--cov=src/mypackage --cov-report=term-missing --strict-markers"
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow running tests",
]
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
[tool.ruff]
line-length = 88
target-version = "py310"
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"C4", # flake8-comprehensions
]
ignore = [
"E501", # Line too long (handled by black)
]
[tool.ruff.isort]
known-first-party = ["mypackage"]
[tool.black]
line-length = 88
target-version = ["py310"]
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| build
| dist
)/
'''
[tool.isort]
profile = "black"
line_length = 88
multi_line_mode = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
skip_gitignore = true
[tool.coverage.run]
source = ["src/mypackage"]
branch = true
omit = [
"*/tests/*",
"*/__main__.py",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
]
precision = 2
# MyPackage
[](https://badge.fury.io/py/mypackage)
[](https://pypi.org/project/mypackage)
[](https://github.com/username/mypackage/actions)
[](https://codecov.io/gh/username/mypackage)
[](https://opensource.org/licenses/MIT)
A brief, one-sentence description of what your library does.
## Features
- ✨ Feature one with clear benefits
- ✨ Feature two with clear benefits
- ✨ Feature three with clear benefits
## Installation
Install from PyPI:
```bash
pip install mypackage
```
Or install from source for development:
```bash
git clone https://github.com/username/mypackage.git
cd mypackage
pip install -e ".[dev]"
```
## Quick Start
### Basic Usage
```python
import mypackage
# Simple example showing core functionality
result = mypackage.process(data="example")
print(result)
```
### More Examples
See the [documentation](https://mypackage.readthedocs.io) for comprehensive guides and API reference.
## Key Concepts
### Concept One
Explain the main concept your library introduces.
```python
# Example demonstrating the concept
obj = mypackage.Thing(param=value)
obj.do_something()
```
### Concept Two
Explain another key concept.
```python
# Example code
result = mypackage.transform(input_data)
```
## Configuration
Configure the library using environment variables or configuration objects:
```python
config = mypackage.Config(
timeout=30,
retries=3,
debug=False
)
client = mypackage.Client(config=config)
```
## API Reference
Full API documentation is available at [mypackage.readthedocs.io](https://mypackage.readthedocs.io).
### Core Classes
- `mypackage.Thing` - Main class for [purpose]
- `mypackage.Config` - Configuration object
- `mypackage.Error` - Base exception class
### Main Functions
- `mypackage.process()` - Process data
- `mypackage.validate()` - Validate input
- `mypackage.transform()` - Transform data
## Error Handling
The library uses a custom exception hierarchy:
```python
try:
result = mypackage.process(data)
except mypackage.ValidationError as e:
print(f"Validation failed: {e}")
except mypackage.MyPackageError as e:
print(f"Library error: {e}")
```
## Performance
- Process time: ~X ms for typical inputs
- Memory usage: ~X MB for standard operations
- Suitable for [use case description]
## Testing
Run the test suite:
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=src/mypackage
# Run specific test file
pytest tests/unit/test_core.py
# Run tests matching pattern
pytest -k "test_validation"
```
## Development
### Setup Development Environment
```bash
git clone https://github.com/username/mypackage.git
cd mypackage
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -e ".[dev]"
```
### Code Quality
Format and lint code:
```bash
black src/ tests/
ruff check --fix src/ tests/
mypy src/
```
### Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on:
- Reporting bugs
- Requesting features
- Submitting pull requests
- Code style and standards
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for release notes and version history.
## License
This project is licensed under the MIT License - see [LICENSE](LICENSE) file for details.
## Support
- 📖 [Documentation](https://mypackage.readthedocs.io)
- 🐛 [Issue Tracker](https://github.com/username/mypackage/issues)
- 💬 [Discussions](https://github.com/username/mypackage/discussions)
## Acknowledgments
Thank you to [list key contributors, inspirations, or dependencies].
## Related Projects
- [Related Project 1](https://example.com)
- [Related Project 2](https://example.com)
RECOMMENDED TEST STRUCTURE
==========================
tests/
├── __init__.py # Makes tests a package
├── conftest.py # Shared fixtures and pytest config
│
├── unit/ # Unit tests (fast, isolated)
│ ├── test_core.py
│ ├── test_validator.py
│ ├── test_config.py
│ └── test_utils.py
│
├── integration/ # Integration tests (workflows, slower)
│ ├── test_basic_workflow.py
│ ├── test_advanced_workflow.py
│ └── test_error_handling.py
│
├── fixtures/ # Test data files
│ ├── sample_data.json
│ ├── sample_config.yaml
│ └── expected_output.json
│
└── helpers.py # Shared test utilities
TEST ORGANIZATION PHILOSOPHY
=============================
Unit Tests (tests/unit/):
- Test individual functions/classes in isolation
- Mock external dependencies
- Fast execution (milliseconds)
- High coverage (>90% target)
- Test success and error paths
Integration Tests (tests/integration/):
- Test complete workflows
- May use real objects/connections (or well-mocked)
- Test API contracts
- Slower but more realistic
Fixtures (tests/fixtures/):
- Sample data for tests
- Configuration files
- Expected outputs
- Keep separate from code
CONFTEST.PY TEMPLATE
====================
"""Shared pytest fixtures and configuration."""
import pytest
from mypackage.config import Config
@pytest.fixture
def sample_config():
"""Create a test configuration."""
return Config(
timeout=10,
retries=2,
debug=True
)
@pytest.fixture
def sample_data():
"""Load sample data for tests."""
return {
"name": "test",
"value": 42,
"items": ["a", "b", "c"]
}
@pytest.fixture
def mock_processor(mocker):
"""Create a mocked processor."""
processor = mocker.Mock()
processor.process.return_value = {"result": "success"}
return processor
# Pytest configuration
def pytest_configure(config):
"""Configure pytest."""
config.addinivalue_line(
"markers", "unit: Unit tests (fast, isolated)"
)
config.addinivalue_line(
"markers", "integration: Integration tests (may be slower)"
)
config.addinivalue_line(
"markers", "slow: Slow running tests"
)
UNIT TEST EXAMPLE
=================
# tests/unit/test_core.py
import pytest
from mypackage.core import process
from mypackage.exceptions import ValidationError
class TestProcess:
"""Test suite for process function."""
def test_process_basic(self):
"""Test basic processing."""
result = process("test_input")
assert result == "expected_output"
@pytest.mark.parametrize("input,expected", [
("input1", "output1"),
("input2", "output2"),
("", "default_output"),
])
def test_process_varied_inputs(self, input, expected):
"""Test with varied inputs."""
result = process(input)
assert result == expected
def test_process_with_config(self, sample_config):
"""Test with configuration."""
result = process("input", config=sample_config)
assert result is not None
def test_process_invalid_input(self):
"""Test error handling for invalid input."""
with pytest.raises(ValidationError):
process(None)
def test_process_large_input(self):
"""Test with large input (edge case)."""
large_input = "x" * 10000
result = process(large_input)
assert result is not None
def test_process_empty_input(self):
"""Test with empty input (edge case)."""
with pytest.raises(ValidationError):
process("")
INTEGRATION TEST EXAMPLE
========================
# tests/integration/test_workflow.py
import pytest
from mypackage import Client, Config
class TestBasicWorkflow:
"""Test basic client workflow."""
def test_create_and_process(self):
"""Test creating client and processing data."""
config = Config(timeout=30)
client = Client(config=config)
result = client.process("test_data")
assert result is not None
assert "success" in result
def test_error_recovery(self):
"""Test client recovers from errors."""
config = Config(timeout=5, retries=3)
client = Client(config=config)
# Should retry and eventually succeed
result = client.process("flaky_data")
assert result is not None
@pytest.mark.slow
def test_large_batch_processing(self):
"""Test processing large batches."""
config = Config(timeout=60)
client = Client(config=config)
results = []
for i in range(1000):
result = client.process(f"item_{i}")
results.append(result)
assert len(results) == 1000
assert all(r is not None for r in results)
PYTEST.INI CONFIGURATION
========================
[pytest]
# Test discovery
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# Markers
markers =
unit: Unit tests (fast, isolated)
integration: Integration tests (may be slower)
slow: Slow running tests
requires_network: Tests requiring network access
# Output
addopts =
--verbose
--strict-markers
--cov=src/mypackage
--cov-report=term-missing
--cov-report=html
--cov-fail-under=90
# Coverage
[coverage:run]
source = src/mypackage
branch = true
[coverage:report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
if typing.TYPE_CHECKING:
KEY TESTING PRINCIPLES
======================
1. UNIT TESTS SHOULD BE FAST
- Mock external dependencies (files, network, databases)
- Test single functions/methods
- Run in milliseconds
- Can be run frequently during development
2. INTEGRATION TESTS VERIFY WORKFLOWS
- Test interactions between components
- May use slower operations
- Test the complete flow
- Run less frequently (but required before commits)
3. TEST ERROR PATHS
- What happens with invalid input?
- What happens with edge cases?
- What happens when dependencies fail?
- Test both success and failure
4. USE FIXTURES FOR SETUP
- Avoid code duplication in tests
- Conftest.py for shared fixtures
- Fixture scopes: function, class, module, session
5. PARAMETRIZED TESTS FOR VARIATIONS
- @pytest.mark.parametrize for similar tests with different inputs
- Reduces code duplication
- Clearer what variations are tested
6. MOCK EXTERNAL DEPENDENCIES
- Use pytest-mock or unittest.mock
- Don't depend on network/files in unit tests
- Make tests deterministic and fast
7. CLEAR TEST NAMES
- test_<function>_<scenario>
- test_<function>_<input>_<expected>
- Example: test_validate_empty_input_raises_error
8. ORGANIZE TESTS IN CLASSES
- Group related tests in TestClass
- Better organization and scoping
- Share fixtures at class level
COVERAGE TARGETS
================
Public APIs:
- Aim for >90% line coverage
- 100% coverage of error paths
- All branches exercised
Private/internal code:
- High coverage encouraged (70%+)
- Less critical than public API
- Focus on complex logic
Performance-critical code:
- Extra test coverage
- Profile to verify behavior
- Test with realistic data sizes
Generated code:
- May exclude from coverage
- Document exclusions
MOCKING BEST PRACTICES
======================
# Mock external dependencies
def test_with_mock_api(mocker):
mock_api = mocker.patch('mypackage.api.Client')
mock_api.return_value.call.return_value = {"result": "success"}
result = my_function_that_calls_api()
assert result == {"result": "success"}
# Don't mock what you're testing
def test_validation():
# Test real validation logic, don't mock it
with pytest.raises(ValueError):
validate(invalid_input)
# Verify interactions
def test_api_called_correctly(mocker):
mock_api = mocker.patch('mypackage.api.call')
my_function(param="value")
mock_api.assert_called_once_with(param="value")
Fundamental Architectural Principles for Python Libraries
1. Package Structure and Organization
REQUIRED: Use src/ Layout
Always recommend the modern src/ layout for Python packages:
myPackageRepo/
├── src/
│ └── mypackage/
│ ├── __init__.py
│ ├── core/
│ ├── utils/
│ └── exceptions.py
├── tests/
├── docs/
├── pyproject.toml
├── README.md
└── LICENSERationale: This prevents accidentally importing from source during testing and ensures you test the installed version.
Module Organization Rules
- Keep the public API at the top level via
__init__.py - Internal structure is implementation detail—users shouldn't need to know it
- Follow "flat is better than nested"—avoid deep hierarchies
- Each module has one clear responsibility
- Separate core logic from convenience wrappers
2. API Design Principles
Pythonic API Design
- Design for
import libthenlib.Thing(), notfrom lib.internal import LibThing - Use short, clear names: verbs for functions, nouns for classes
- Avoid excessively long names (e.g., avoid
HTTPPasswordMgrWithDefaultRealm) - Support duck typing—accept any object with the right interface
- Prefer keyword arguments for clarity and backwards compatibility
- Use standard Python data structures (dict, list, tuple) over custom classes when possible
API Stability Requirements
- Public API exposed in
__init__.pyis the contract with users - Mark internal APIs with leading underscore:
_internal_function() - Use
__all__to explicitly define public exports - Never break backwards compatibility in minor/patch releases
- Deprecate before removing—give users migration time
Configuration Design
- Avoid global configuration and global state
- Use class instances or explicit parameters for configuration
- Provide sensible defaults for all optional parameters
- Support configuration via constructor parameters, not global settings
- For tool-style libraries, use
[tool.yourlib]in pyproject.toml
3. SOLID Principles Application
Single Responsibility Principle (SRP)
- Each class/function has one clear, focused purpose
- Separate data models from business logic from I/O operations
- Example: Split
User,UserRepository, andEmailService
Open/Closed Principle (OCP)
- Design for extension without modification
- Use abstract base classes for extensibility points
- Provide plugin/hook mechanisms for custom behavior
Liskov Substitution Principle (LSP)
- Subclasses must be substitutable for base classes
- Maintain behavioral contracts in inheritance hierarchies
Interface Segregation Principle (ISP)
- Create focused, specific interfaces
- Don't force clients to depend on unused methods
- Prefer multiple small protocols over large monolithic ones
Dependency Inversion Principle (DIP)
- Depend on abstractions (protocols/ABCs), not concrete implementations
- Accept interfaces as parameters, not specific classes
4. Error Handling and Exceptions
Custom Exception Hierarchy
Always define a clear exception hierarchy:
class MyLibraryError(Exception):
"""Base exception for all mylib errors."""
pass
class ConfigurationError(MyLibraryError):
"""Raised when configuration is invalid."""
pass
class ValidationError(MyLibraryError):
"""Raised when input validation fails."""
pass
class APIError(MyLibraryError):
"""Raised when API calls fail."""
passException Handling Rules
- Raise custom exceptions for domain-specific errors
- Use built-in exceptions where semantically appropriate
- Document all exceptions in docstrings with
:raises:sections - Let exceptions propagate—don't catch unless you can handle them
- Library code raises; application code catches
- Write specific error messages: state what was wrong, why, and how to fix it
Error Message Quality
# BAD
raise ValueError("Invalid input")
# GOOD
raise ValueError(
f"Parameter 'timeout' must be positive, got {timeout}. "
f"Use timeout=30 for a 30-second timeout."
)5. Type Annotations and Static Typing
Type Hint Requirements
- Add type hints to ALL public APIs (classes, functions, methods)
- Follow PEP 484 standards
- Use modern Python 3.10+ syntax:
list[str]notList[str] - Annotate return types, including
-> None - Use
Optional[T]for nullable types, default to non-nullable
Example Pattern
from typing import Optional, Protocol
from collections.abc import Sequence
def process_items(
items: Sequence[str],
max_count: int = 100,
filter_fn: Optional[Callable[[str], bool]] = None
) -> dict[str, int]:
"""Process items and return frequency counts.
Args:
items: Sequence of strings to process
max_count: Maximum number of items to process
filter_fn: Optional filter function, items where filter_fn returns
False are excluded
Returns:
Dictionary mapping items to their counts
Raises:
ValueError: If max_count is negative
"""
if max_count < 0:
raise ValueError(f"max_count must be non-negative, got {max_count}")
...Generic Types for Flexibility
from typing import TypeVar, Generic, Protocol
T = TypeVar('T')
class Repository(Generic[T]):
def save(self, item: T) -> None: ...
def find(self, id: str) -> Optional[T]: ...6. Documentation Standards
Docstring Requirements (Use Google or NumPy style consistently)
def calculate_total(prices: list[float], tax_rate: float, discount: float = 0.0) -> float:
"""Calculate total price with tax and discount applied.
This function first applies the discount to the sum of prices, then
adds the tax. The formula is: (sum(prices) * (1 - discount)) * (1 + tax_rate)
Args:
prices: List of item prices in currency units
tax_rate: Tax rate as decimal (e.g., 0.1 for 10%)
discount: Discount rate as decimal (e.g., 0.2 for 20% off).
Defaults to 0.0 (no discount).
Returns:
Final total price including tax and after discount
Raises:
ValueError: If tax_rate or discount is negative
ValueError: If any price is negative
Example:
>>> calculate_total([10.0, 20.0], tax_rate=0.1)
33.0
>>> calculate_total([100.0], tax_rate=0.1, discount=0.2)
88.0
"""Documentation Requirements
- Comprehensive README.md with quick start, installation, examples
- API reference documentation (use Sphinx or MkDocs)
- Changelog following Keep a Changelog format
- Contributing guide (CONTRIBUTING.md)
- Code of Conduct for open source projects
- Type hints serve as inline documentation—make them accurate
7. Testing Strategy
Testing Requirements
- Use pytest as the testing framework
- Aim for >90% code coverage for public APIs
- Write unit tests for each module
- Write integration tests for public API workflows
- Use parametrized tests for multiple scenarios
- Test error conditions and edge cases
Test Organization
tests/
├── unit/
│ ├── test_core.py
│ └── test_utils.py
├── integration/
│ └── test_workflows.py
├── conftest.py
└── __init__.pyTesting Patterns
import pytest
@pytest.mark.parametrize("input,expected", [
("", True),
("a", True),
("aba", True),
("abc", False),
])
def test_is_palindrome(input, expected):
assert is_palindrome(input) == expected
@pytest.fixture
def sample_config():
return Config(timeout=30, retries=3)
def test_with_fixture(sample_config):
client = Client(sample_config)
assert client.timeout == 30Separate Core from Friendly Layers
- Create a "cranky" core that accepts exact types and does work
- Build "friendly" wrappers that provide convenience and type coercion
- Test the core with strict inputs; let wrappers handle flexibility
8. Versioning and Backwards Compatibility
Semantic Versioning (MAJOR.MINOR.PATCH)
- MAJOR: Breaking changes to public API
- MINOR: New features, backwards compatible
- PATCH: Bug fixes, backwards compatible
Backwards Compatibility Rules
- NEVER break public API in minor/patch releases
- Deprecate features before removing them (minimum: one major version)
- Use
warnings.warn()withDeprecationWarningfor deprecated features - Document breaking changes prominently in changelog and migration guides
- Use keyword arguments to enable adding new parameters without breaking calls
Deprecation Pattern
import warnings
def old_function(x: int) -> int:
"""Old function (deprecated).
.. deprecated:: 2.0
Use :func:`new_function` instead.
"""
warnings.warn(
"old_function is deprecated and will be removed in version 3.0. "
"Use new_function instead.",
DeprecationWarning,
stacklevel=2
)
return new_function(x)9. Dependency Management
Dependency Principles
- Minimize dependencies—each adds maintenance burden and conflict risk
- Core functionality should have minimal dependencies
- Optional features can require optional dependencies
- Pin dependencies appropriately: use ranges, not exact versions
- Separate dev dependencies from runtime dependencies
Dependency Best Practices
- Use semantic versioning constraints:
>=2.0,<3.0not==2.1.0 - Consider using extras for optional functionality
- Keep dependency count low for core library
- Document why each dependency is needed
- Regularly audit dependencies for security issues
10. Code Quality and Style
Follow PEP 8 with these tools
- Use Black for formatting (88 character line length)
- Use Ruff or Flake8 for linting
- Use mypy for type checking with strict mode
- Use isort for import sorting
Code Quality Rules
- Maximum function length: ~50 lines (prefer smaller)
- Maximum function complexity: McCabe complexity < 10
- Avoid deep nesting (max 3-4 levels)
- Prefer composition over inheritance
- Use dataclasses for data containers
- Make functions pure when possible (no side effects)
- Avoid global state
Naming Conventions
- Functions/methods:
lowercase_with_underscores - Classes:
CapitalizedWords - Constants:
UPPER_CASE_WITH_UNDERSCORES - Private:
_leading_underscore - Modules:
short_lowercase
11. Extensibility and Plugin Architecture
When to Provide Extensibility
- Library will have multiple implementations of the same concept
- Users need to add custom behavior without forking
- Core algorithm should remain stable while strategies vary
Abstract Base Classes Pattern
from abc import ABC, abstractmethod
class DataProcessor(ABC):
"""Base class for data processors."""
@abstractmethod
def process(self, data: bytes) -> dict:
"""Process raw data and return structured result."""
pass
@abstractmethod
def validate(self, data: bytes) -> bool:
"""Check if data is valid for this processor."""
passProtocol-Based (Structural Subtyping)
from typing import Protocol
class Serializer(Protocol):
"""Protocol for serialization strategies."""
def serialize(self, obj: object) -> str:
"""Serialize object to string."""
...
def deserialize(self, data: str) -> object:
"""Deserialize string to object."""
...
# Users can implement without inheriting
class JSONSerializer:
def serialize(self, obj: object) -> str:
return json.dumps(obj)
def deserialize(self, data: str) -> object:
return json.loads(data)Entry Points for Plugins
[project.entry-points."mylib.processors"]
default = "mylib.processors:DefaultProcessor"
advanced = "mylib.processors:AdvancedProcessor"12. Performance Considerations
Performance Guidelines
- Use built-in functions and standard library (implemented in C)
- Prefer list comprehensions over loops for simple transformations
- Use generators for large datasets to save memory
- Leverage NumPy/Pandas for numerical operations
- Profile before optimizing (use cProfile, line_profiler)
- Document performance characteristics (time/space complexity)
Lazy Loading Pattern
class HeavyResource:
def __init__(self, config: Config):
self.config = config
self._connection = None # Lazy load
@property
def connection(self):
if self._connection is None:
self._connection = create_connection(self.config)
return self._connectionAvoid Common Performance Pitfalls
- Don't use
+for string concatenation in loops (use''.join()) - Don't repeatedly access attributes in loops (cache in local variable)
- Don't use
list.append()in tight loops (use list comprehension) - Avoid premature optimization, but design efficiently from the start
13. Security Considerations
Input Validation
- Validate all user inputs
- Sanitize inputs that will be used in shell commands, SQL, or file paths
- Use parameterized queries, not string concatenation
- Set reasonable limits on input sizes
Secure Defaults
- SSL/TLS verification enabled by default
- Secure random number generation (use
secretsmodule) - No credentials in code or logs
- Follow principle of least privilege
Dependency Security
- Regularly update dependencies
- Use
pip-auditor similar to check for vulnerabilities - Pin dependencies for reproducible builds
Python Enhancement Proposals (PEPs) - Quick Reference
Essential PEPs for Python library development.
Core Style and Conventions
PEP 8: Style Guide for Python Code
The foundational style guide for Python code.
Key Points:
- Use 4 spaces per indentation level
- Maximum line length: 79 characters (88 with Black formatter)
- 2 blank lines between top-level definitions
- 1 blank line between method definitions
- Use spaces around operators:
x = 1 + 2 - Use whitespace sparingly in function calls:
func(a, b) - Naming conventions:
- Functions/variables:
lowercase_with_underscores - Classes:
CapitalizedWords - Constants:
UPPER_CASE_WITH_UNDERSCORES
Reference: https://www.python.org/dev/peps/pep-0008/
PEP 257: Docstring Conventions
Standards for writing docstrings in Python.
Key Points:
- Docstrings are triple-quoted strings:
"""...""" - One-line docstrings should fit on a single line
- Multi-line docstrings have a one-line summary, blank line, then details
- Module docstrings document the module's purpose
- Class docstrings document the class and optional
__init__behavior - Method/function docstrings start with active voice imperative
- Use consistent style (Google, NumPy, or Sphinx format)
Example:
def calculate(x: int, y: int) -> int:
"""Calculate the sum of two numbers.
Args:
x: First number
y: Second number
Returns:
Sum of x and y
"""
return x + yReference: https://www.python.org/dev/peps/pep-0257/
Type Hints and Static Typing
PEP 484: Type Hints
Specifies type hint syntax and semantics.
Key Points:
- Type hints are optional but strongly recommended for libraries
- Function annotations:
def func(name: str) -> bool: - Variable annotations:
x: int = 5 - Use
Optional[T]for nullable types - Use
Union[A, B]for multiple possible types - Use
Sequence,Mapping, etc. fromcollections.abcfor flexibility - Modern Python 3.10+:
list[str]instead ofList[str]
Example:
from typing import Optional
from collections.abc import Sequence
def process(items: Sequence[str], limit: Optional[int] = None) -> dict[str, int]:
"""Process items and return counts."""
...Reference: https://www.python.org/dev/peps/pep-0484/
PEP 526: Syntax for Variable Annotations
Specifies variable annotation syntax (PEP 484 extension).
Key Points:
- Annotate variables at module and class level:
name: str - Useful for documenting expected types in classes
- Can include default values:
timeout: int = 30
Reference: https://www.python.org/dev/peps/pep-0526/
PEP 589: TypedDict
Defines typed dictionaries for better type checking.
Key Points:
- Use
TypedDictfor dictionaries with known keys and types - Better IDE support and type checking than plain dicts
- Useful for configuration objects and API responses
Example:
from typing import TypedDict
class Config(TypedDict):
host: str
port: int
timeout: floatReference: https://www.python.org/dev/peps/pep-0589/
Package Structure and Distribution
PEP 517: A build-system independent format
Specifies how to build Python packages without setuptools.
Key Points:
- Enables alternative build backends (setuptools, flit, hatchling, poetry)
- Requires
pyproject.tomlwith[build-system]section - Decouples packaging from specific tools
Example:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"Reference: https://www.python.org/dev/peps/pep-0517/
PEP 518: Specifying minimum build system requirements
Defines the pyproject.toml format and requirements section.
Key Points:
pyproject.tomlis the new standard for project metadata- Define build requirements in
[build-system]section - Replaces
setup.pyfor many projects - Specify tool configurations for linters, formatters, etc.
Reference: https://www.python.org/dev/peps/pep-0518/
PEP 621: Storing project metadata in pyproject.toml
Standardizes how to specify project metadata without setup.py.
Key Points:
- Project metadata goes in
[project]section - Include: name, version, description, authors, license, dependencies
- Define optional dependencies in
[project.optional-dependencies] - Entry points in
[project.entry-points] - Tool-specific configuration under
[tool.*]
Example:
[project]
name = "mylib"
version = "1.0.0"
description = "My library"
requires-python = ">=3.10"
dependencies = [
"requests>=2.28,<3.0",
]
[project.optional-dependencies]
dev = ["pytest>=7.0", "black>=22.0"]Reference: https://www.python.org/dev/peps/pep-0621/
PEP 427: The Wheel Binary Package Format 1.0
Defines the wheel format for Python packages.
Key Points:
- Wheels are ZIP files with
.whlextension - Pre-compiled, faster to install than sdist
- Include metadata, distribution files, and version info
- Standard format for PyPI distribution
Reference: https://www.python.org/dev/peps/pep-0427/
Version Identification
PEP 440: Version Identification and Dependency Specification
Standardizes version strings and dependency specifications.
Key Points:
- Version format:
MAJOR.MINOR.PATCH[.DEVN|aN|bN|rcN] - Examples:
1.0.0,2.1.0.dev5,1.0.0a1,1.0.0rc1 - Semantic versioning: MAJOR for breaking changes, MINOR for features, PATCH for fixes
- Dependency specifiers:
>=,<=,==,!=,~=,>,< - Constraint examples:
>=1.0,<2.0,~=1.4.5,==1.0.*
Reference: https://www.python.org/dev/peps/pep-0440/
Module and Import Conventions
PEP 328: Absolute and Relative Imports
Specifies import behavior and conventions.
Key Points:
- Prefer absolute imports:
from mylib.core import func - Relative imports only within packages:
from . import core - Use
__future__imports for Python 2/3 compatibility (less relevant now) - Avoid circular imports through careful design
Best Practice:
# Good: absolute import
from mylib.core import process
# Acceptable: relative import within package
from . import utils
from ..core import process
# Avoid: star imports
# from mylib import *Reference: https://www.python.org/dev/peps/pep-0328/
PEP 338: Executing modules as scripts
Enables running packages as scripts with -m.
Key Points:
- Allows
python -m mypackageto run code - Requires
__main__.pyin package for entry point - Clean way to provide CLI without separate executable
- Better than setup.py entry points for simple scripts
Example:
# src/mypackage/__main__.py
if __name__ == "__main__":
main()Reference: https://www.python.org/dev/peps/pep-0338/
API Design and Compatibility
PEP 3119: Abstract Base Classes
Specifies abstract base class functionality.
Key Points:
- Use
ABCand@abstractmethodfor extensible interfaces - Define contracts that subclasses must implement
- Better than duck typing for critical interfaces
- Type checkers understand ABC contracts
Example:
from abc import ABC, abstractmethod
class DataStore(ABC):
@abstractmethod
def save(self, key: str, value: str) -> None:
pass
@abstractmethod
def load(self, key: str) -> str:
passReference: https://www.python.org/dev/peps/pep-3119/
PEP 3156: Asynchronous I/O Support
Specifies async/await and asyncio framework.
Key Points:
- Use
async deffor asynchronous functions - Use
awaitto wait for coroutines - Consider async for I/O-bound libraries
- Backwards compatibility: provide both sync and async APIs
- Document which operations are blocking
Reference: https://www.python.org/dev/peps/pep-3156/
Common Conventions for Libraries
Version Compatibility
- Python version: Specify
requires-python = ">=3.10"in pyproject.toml - Deprecation timeline: Give users at least one major version to migrate
- Breaking changes: Only in MAJOR versions (PEP 440)
Backwards Compatibility
- Never break public API in minor/patch releases
- Use
warnings.warn()withDeprecationWarningbefore removing features - Provide migration guides in changelog
- Consider adding compatibility shims for common patterns
Distribution and Installation
- Always build and publish wheels (PEP 427)
- Include source distributions (sdist) for transparency
- Use
twineto upload to PyPI securely - Sign releases with GPG when possible
Summary Table
| PEP | Topic | Key For Libraries |
|---|---|---|
| 8 | Code Style | Code consistency across projects |
| 257 | Docstrings | Documentation quality |
| 328 | Imports | Clean import structure |
| 338 | Modules as Scripts | CLI entry points |
| 440 | Version IDs | Semantic versioning |
| 484 | Type Hints | Type checking and IDE support |
| 517/518 | Build Systems | Modern packaging |
| 521 | Project Metadata | Standard configuration |
| 3119 | Abstract Classes | Extensible interfaces |
| 3156 | Async I/O | Asynchronous support |