Claude-skill-registry class-design
Python class design conventions for this codebase. Apply when writing or reviewing classes including interfaces, inheritance, composition, and attribute access.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/class-design" ~/.claude/skills/majiayu000-claude-skill-registry-class-design && rm -rf "$T"
manifest:
skills/data/class-design/SKILL.mdsource content
Class Design Conventions
Favor composition over inheritance. Use Protocol classes for interfaces and dependency injection for shared behavior.
Quick Reference
| Principle | Pattern |
|---|---|
| Interfaces | classes for duck typing |
| Shared behavior | Dependency injection or mixins |
| Inheritance depth | Maximum 2 levels |
| Framework base classes | OK to inherit (, ) |
| Concrete inheritance | Forbidden—use mixins instead |
| Static methods | Avoid—use module-level functions |
| Internal APIs | Design for extension, not restriction |
| Private attributes | Only to avoid subclass naming conflicts |
| Getters/setters | Use plain attributes instead |
Protocol Classes for Interfaces
Use
Protocol to define interfaces. This enables duck typing with static type checking.
# CORRECT - Protocol defines the interface from typing import Protocol class Tokenizer(Protocol): """Interface for text tokenization.""" def tokenize(self, text: str) -> list[str]: """Split text into tokens.""" ... def detokenize(self, tokens: list[str]) -> str: """Join tokens back into text.""" ... class SimpleTokenizer: """Whitespace tokenizer implementing Tokenizer protocol.""" def tokenize(self, text: str) -> list[str]: return text.split() def detokenize(self, tokens: list[str]) -> str: return " ".join(tokens) def process_text(tokenizer: Tokenizer, text: str) -> list[str]: """Works with any Tokenizer implementation.""" return tokenizer.tokenize(text.lower()) # INCORRECT - abstract base class forces explicit inheritance from abc import ABC, abstractmethod class Tokenizer(ABC): @abstractmethod def tokenize(self, text: str) -> list[str]: pass class SimpleTokenizer(Tokenizer): # Must explicitly inherit ...
When to Use Protocol vs ABC
| Use Case | Choice |
|---|---|
| Define interface for type checking | |
| Duck typing with static analysis | |
Need checks at runtime | with |
| Framework requires inheritance | |
Composition via Dependency Injection
Inject dependencies rather than inheriting behavior.
# CORRECT - composition via dependency injection from dataclasses import dataclass @dataclass class DocumentProcessor: """Processes documents using injected components.""" tokenizer: Tokenizer embedder: Embedder storage: Storage def process(self, doc: Document) -> ProcessedDocument: tokens = self.tokenizer.tokenize(doc.content) embedding = self.embedder.embed(tokens) self.storage.save(doc.id, embedding) return ProcessedDocument(doc.id, tokens, embedding) # Usage - compose with specific implementations processor = DocumentProcessor( tokenizer=SimpleTokenizer(), embedder=OpenAIEmbedder(api_key=settings.api_key), storage=RedisStorage(url=settings.redis_url), ) # INCORRECT - inheriting behavior from multiple classes class DocumentProcessor(TokenizerMixin, EmbedderMixin, StorageMixin): def process(self, doc: Document) -> ProcessedDocument: tokens = self.tokenize(doc.content) # Where does this come from? ...
Inheritance Rules
Maximum Depth: 2 Levels
# CORRECT - shallow hierarchy class BaseHandler: """Base handler with common functionality.""" ... class FileHandler(BaseHandler): """Handles file operations.""" ... # INCORRECT - too deep (3+ levels) class BaseHandler: ... class IOHandler(BaseHandler): ... class FileHandler(IOHandler): # Third level - forbidden ...
Framework Base Classes: Allowed
Inherit from framework base classes that provide essential functionality.
# CORRECT - inheriting from framework base classes from pydantic import BaseModel from torch import nn class UserConfig(BaseModel): """User configuration with Pydantic validation.""" name: str max_retries: int = 3 class Encoder(nn.Module): """Neural network encoder layer.""" def __init__(self, input_dim: int, output_dim: int) -> None: super().__init__() self.linear = nn.Linear(input_dim, output_dim) def forward(self, x: Tensor) -> Tensor: return self.linear(x)
Concrete Class Inheritance: Forbidden
Never inherit from concrete (non-abstract, non-framework) classes. Use mixins or composition.
# INCORRECT - inheriting from concrete class class FileProcessor: def process(self, path: Path) -> Data: content = path.read_text() return self.parse(content) def parse(self, content: str) -> Data: ... class JsonProcessor(FileProcessor): # Forbidden - FileProcessor is concrete def parse(self, content: str) -> Data: return json.loads(content) # CORRECT - use composition class JsonParser: """Parses JSON content.""" def parse(self, content: str) -> Data: return json.loads(content) @dataclass class FileProcessor: """Processes files using an injected parser.""" parser: Parser def process(self, path: Path) -> Data: content = path.read_text() return self.parser.parse(content) # Usage processor = FileProcessor(parser=JsonParser())
Mixins for Shared Behavior
Use mixins only when composition isn't practical. Mixins should:
- Be small and focused on one capability
- Not maintain state
- Use clear naming (
suffix)*Mixin
# CORRECT - focused mixin for logging class LoggingMixin: """Adds structured logging to a class.""" @property def logger(self) -> Logger: return logging.getLogger(self.__class__.__name__) def log_operation(self, operation: str, **context: Any) -> None: self.logger.info(operation, extra=context) class DataLoader(LoggingMixin): """Loads data with logging support.""" def load(self, path: Path) -> Data: self.log_operation("load_start", path=str(path)) data = self._load_impl(path) self.log_operation("load_complete", path=str(path), size=len(data)) return data
Avoid Static Methods
Use module-level functions instead of
@staticmethod.
# INCORRECT - static method class MathUtils: @staticmethod def clamp(value: float, min_val: float, max_val: float) -> float: return max(min_val, min(value, max_val)) # CORRECT - module-level function def clamp(value: float, min_val: float, max_val: float) -> float: """Clamp value to the range [min_val, max_val].""" return max(min_val, min(value, max_val))
Why avoid static methods?
- They don't use class or instance state
- Module functions are simpler and more Pythonic
- Easier to import and test
Plain Attributes Over Getters/Setters
Use plain attributes. Add
@property only when you need computed values or validation.
# CORRECT - plain attributes @dataclass class User: """User with plain attributes.""" name: str email: str age: int # CORRECT - property for computed value class Rectangle: """Rectangle with computed area property.""" def __init__(self, width: float, height: float) -> None: self.width = width self.height = height @property def area(self) -> float: """Computed from width and height.""" return self.width * self.height # INCORRECT - unnecessary getters/setters class User: def __init__(self, name: str) -> None: self._name = name def get_name(self) -> str: return self._name def set_name(self, name: str) -> None: self._name = name
Decision Flow
Need to define an interface? ├── Yes → Use Protocol └── No → Need shared behavior? ├── Yes → Can inject as dependency? │ ├── Yes → Use composition (dependency injection) │ └── No → Use a focused Mixin └── No → Inheriting from framework base class? ├── Yes → OK (BaseModel, nn.Module, etc.) └── No → Is it concrete? ├── Yes → Forbidden - refactor to composition └── No → OK if depth ≤ 2 levels