python-design-patterns
Skill1 starsChanged 4 months agoMITView on GitHub
Python design patterns including KISS, Separation of Concerns, Single Responsibility, and composition over inheritance. Use this skill when designing a new service or component from scratch and choosing how to layer responsibilities, when refactoring a God class or monolithic function that has grown too large, when deciding whether to add a new abstraction or live with duplication, when evaluating a pull request for structural issues like tight coupling or leaking internal types, when choosing between inheritance and composition for a new class hierarchy, or when a codebase is becoming hard to test because of entangled I/O and business logic.
15 other projects use a version of this, 12 of them edited.
Adopted by
Most recently taken up by
- zebbern/pocmapedited, first seen Nov 8, 2026
- delorenj/skillsedited, first seen Nov 7, 2026
- nledford/engineering-review-boardtheir own version, first seen Nov 3, 2026
- scwf/feishuai-skillsedited, first seen Oct 25, 2026
- anton-karlovskiy/deal-hunter-agentsedited, first seen Oct 13, 2026
- enes9103/my-claude-agents-skillsedited, first seen Sep 28, 2026
Used with
Projects with it also use javascript-testing-patterns, nodejs-backend-patterns and 1 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
xfstudioSkilljavascript-testing-patterns
Implement comprehensive testing strategies using Jest, Vitest, and Testing Library for unit tests, integration tests, and end-to-end testing with mocking, fixtures, and test-driven development. Use when writing JavaScript/TypeScript tests, setting up test infrastructure, or implementing TDD/BDD workflows.
Also in 3 projects that use this
xfstudioSkillnodejs-backend-patterns
Build production-ready Node.js backend services with Express/Fastify, implementing middleware patterns, error handling, authentication, database integration, and API design best practices. Use when creating Node.js servers, REST APIs, GraphQL backends, or microservices architectures.
Also in 3 projects that use this
- +1 more from xfstudio
Other versions15 projects use a version of this.
Word for word · 1
- zebbern/termstack★ 4100% same
Edited · 12
- delorenj/skills★ 1799% same
- zebbern/pocmap★ 796% same
- GPTtang/skill-atlas★ 499% same
- scwf/feishuai-skills★ 499% same
- Aver005/agent-skills★ 299% same
- CodeWithBehnam/cc-docs★ 299% same
- NorkzYT/claude-code-autopilot★ 299% same
- roshankodi/agent-skills★ 296% same
- anton-karlovskiy/deal-hunter-agents★ 196% same
- squarerf/claude-skills★ 196% same
2 more not listed
Wrote their own version · 2
- nledford/engineering-review-board★ 11same name
- 2867203296/hermes-agent★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository18 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- architecture-patternsSkill
- async-python-patternsSkill
- design-mdSkill
- python-anti-patternsSkill
- python-code-styleSkill
- python-configurationSkill
- python-error-handlingSkill
- python-observabilitySkill
- python-packagingSkill
- python-performance-optimizationSkill
- python-resource-managementSkill
- python-testing-patternsSkill
- python-type-safetySkill
- shellcheck-configurationSkill
- temporal-python-testingSkill
- uv-package-managerSkill
The file
We split this skill into the rules it gives an agent
8 rules. 4 are common, 4 are distinctive, and 0 are very specific to this project.
4
Common
Most of the 13 edited versions we read keep these.
4
Distinctive
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Common: line 8
Write maintainable Python code using fundamental design principles.
- Common: line 35
Wait until you have three instances before abstracting.
- Distinctive: line 52
Before adding complexity, ask: does a simpler solution work?
- Distinctive: line 254
Wait until you have three instances before abstracting.
- Distinctive: line 284
Keep functions focused. Extract when a function:
- Common: line 406
Rule of three: - Wait before abstracting
- Common: line 409
Delete before abstracting: - Remove dead code, then consider patterns
- Distinctive: line 422
Keep the composition shallow (2-3 levels).
“Common” is measured: kept by at least 6 in 10 of the 13 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Python Design Patterns
Write maintainable Python code using fundamental design principles. These patterns help you build systems that are easy to understand, test, and modify.
When to Use This Skill
- Designing new components or services
- Refactoring complex or tangled code
- Deciding whether to create an abstraction
- Choosing between inheritance and composition
- Evaluating code complexity and coupling
- Planning modular architectures
Core Concepts
1. KISS (Keep It Simple)
Choose the simplest solution that works. Complexity must be justified by concrete requirements.
2. Single Responsibility (SRP)
Each unit should have one reason to change. Separate concerns into focused components.
3. Composition Over Inheritance
Build behavior by combining objects, not extending classes.
4. Rule of Three
Wait until you have three instances before abstracting. Duplication is often better than premature abstraction.
Quick Start
# Simple beats clever
# Instead of a factory/registry pattern:
FORMATTERS = {"json": JsonFormatter, "csv": CsvFormatter}
def get_formatter(name: str) -> Formatter:
return FORMATTERS[name]()Fundamental Patterns
Pattern 1: KISS - Keep It Simple
Before adding complexity, ask: does a simpler solution work?
# Over-engineered: Factory with registration
class OutputFormatterFactory:
_formatters: dict[str, type[Formatter]] = {}
@classmethod
def register(cls, name: str):
def decorator(formatter_cls):
cls._formatters[name] = formatter_cls
return formatter_cls
return decorator
@classmethod
def create(cls, name: str) -> Formatter:
return cls._formatters[name]()
@OutputFormatterFactory.register("json")
class JsonFormatter(Formatter):
...
# Simple: Just use a dictionary
FORMATTERS = {
"json": JsonFormatter,
"csv": CsvFormatter,
"xml": XmlFormatter,
}
def get_formatter(name: str) -> Formatter:
"""Get formatter by name."""
if name not in FORMATTERS:
raise ValueError(f"Unknown format: {name}")
return FORMATTERS[name]()The factory pattern adds code without adding value here. Save patterns for when they solve real problems.
Pattern 2: Single Responsibility Principle
Each class or function should have one reason to change.
# BAD: Handler does everything
class UserHandler:
async def create_user(self, request: Request) -> Response:
# HTTP parsing
data = await request.json()
# Validation
if not data.get("email"):
return Response({"error": "email required"}, status=400)
# Database access
user = await db.execute(
"INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *",
data["email"], data["name"]
)
# Response formatting
return Response({"id": user.id, "email": user.email}, status=201)
# GOOD: Separated concerns
class UserService:
"""Business logic only."""
def __init__(self, repo: UserRepository) -> None:
self._repo = repo
async def create_user(self, data: CreateUserInput) -> User:
# Only business rules here
user = User(email=data.email, name=data.name)
return await self._repo.save(user)
class UserHandler:
"""HTTP concerns only."""
def __init__(self, service: UserService) -> None:
self._service = service
async def create_user(self, request: Request) -> Response:
data = CreateUserInput(**(await request.json()))
user = await self._service.create_user(data)
return Response(user.to_dict(), status=201)Now HTTP changes don't affect business logic, and vice versa.
Pattern 3: Separation of Concerns
Organize code into distinct layers with clear responsibilities.
┌─────────────────────────────────────────────────────┐
│ API Layer (handlers) │
│ - Parse requests │
│ - Call services │
│ - Format responses │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Service Layer (business logic) │
│ - Domain rules and validation │
│ - Orchestrate operations │
│ - Pure functions where possible │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Repository Layer (data access) │
│ - SQL queries │
│ - External API calls │
│ - Cache operations │
└─────────────────────────────────────────────────────┘Each layer depends only on layers below it:
# Repository: Data access
class UserRepository:
async def get_by_id(self, user_id: str) -> User | None:
row = await self._db.fetchrow(
"SELECT * FROM users WHERE id = $1", user_id
)
return User(**row) if row else None
# Service: Business logic
class UserService:
def __init__(self, repo: UserRepository) -> None:
self._repo = repo
async def get_user(self, user_id: str) -> User:
user = await self._repo.get_by_id(user_id)
if user is None:
raise UserNotFoundError(user_id)
return user
# Handler: HTTP concerns
@app.get("/users/{user_id}")
async def get_user(user_id: str) -> UserResponse:
user = await user_service.get_user(user_id)
return UserResponse.from_user(user)Pattern 4: Composition Over Inheritance
Build behavior by combining objects rather than inheriting.
# Inheritance: Rigid and hard to test
class EmailNotificationService(NotificationService):
def __init__(self):
super().__init__()
self._smtp = SmtpClient() # Hard to mock
def notify(self, user: User, message: str) -> None:
self._smtp.send(user.email, message)
# Composition: Flexible and testable
class NotificationService:
"""Send notifications via multiple channels."""
def __init__(
self,
email_sender: EmailSender,
sms_sender: SmsSender | None = None,
push_sender: PushSender | None = None,
) -> None:
self._email = email_sender
self._sms = sms_sender
self._push = push_sender
async def notify(
self,
user: User,
message: str,
channels: set[str] | None = None,
) -> None:
channels = channels or {"email"}
if "email" in channels:
await self._email.send(user.email, message)
if "sms" in channels and self._sms and user.phone:
await self._sms.send(user.phone, message)
if "push" in channels and self._push and user.device_token:
await self._push.send(user.device_token, message)
# Easy to test with fakes
service = NotificationService(
email_sender=FakeEmailSender(),
sms_sender=FakeSmsSender(),
)Advanced Patterns
Pattern 5: Rule of Three
Wait until you have three instances before abstracting.
# Two similar functions? Don't abstract yet
def process_orders(orders: list[Order]) -> list[Result]:
results = []
for order in orders:
validated = validate_order(order)
result = process_validated_order(validated)
results.append(result)
return results
def process_returns(returns: list[Return]) -> list[Result]:
results = []
for ret in returns:
validated = validate_return(ret)
result = process_validated_return(validated)
results.append(result)
return results
# These look similar, but wait! Are they actually the same?
# Different validation, different processing, different errors...
# Duplication is often better than the wrong abstraction
# Only after a third case, consider if there's a real pattern
# But even then, sometimes explicit is better than abstractPattern 6: Function Size Guidelines
Keep functions focused. Extract when a function:
- Exceeds 20-50 lines (varies by complexity)
- Serves multiple distinct purposes
- Has deeply nested logic (3+ levels)
# Too long, multiple concerns mixed
def process_order(order: Order) -> Result:
# 50 lines of validation...
# 30 lines of inventory check...
# 40 lines of payment processing...
# 20 lines of notification...
pass
# Better: Composed from focused functions
def process_order(order: Order) -> Result:
"""Process a customer order through the complete workflow."""
validate_order(order)
reserve_inventory(order)
payment_result = charge_payment(order)
send_confirmation(order, payment_result)
return Result(success=True, order_id=order.id)Pattern 7: Dependency Injection
Pass dependencies through constructors for testability.
from typing import Protocol
class Logger(Protocol):
def info(self, msg: str, **kwargs) -> None: ...
def error(self, msg: str, **kwargs) -> None: ...
class Cache(Protocol):
async def get(self, key: str) -> str | None: ...
async def set(self, key: str, value: str, ttl: int) -> None: ...
class UserService:
"""Service with injected dependencies."""
def __init__(
self,
repository: UserRepository,
cache: Cache,
logger: Logger,
) -> None:
self._repo = repository
self._cache = cache
self._logger = logger
async def get_user(self, user_id: str) -> User:
# Check cache first
cached = await self._cache.get(f"user:{user_id}")
if cached:
self._logger.info("Cache hit", user_id=user_id)
return User.from_json(cached)
# Fetch from database
user = await self._repo.get_by_id(user_id)
if user:
await self._cache.set(f"user:{user_id}", user.to_json(), ttl=300)
return user
# Production
service = UserService(
repository=PostgresUserRepository(db),
cache=RedisCache(redis),
logger=StructlogLogger(),
)
# Testing
service = UserService(
repository=InMemoryUserRepository(),
cache=FakeCache(),
logger=NullLogger(),
)Pattern 8: Avoiding Common Anti-Patterns
Don't expose internal types:
# BAD: Leaking ORM model to API
@app.get("/users/{id}")
def get_user(id: str) -> UserModel: # SQLAlchemy model
return db.query(UserModel).get(id)
# GOOD: Use response schemas
@app.get("/users/{id}")
def get_user(id: str) -> UserResponse:
user = db.query(UserModel).get(id)
return UserResponse.from_orm(user)Don't mix I/O with business logic:
# BAD: SQL embedded in business logic
def calculate_discount(user_id: str) -> float:
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
orders = db.query("SELECT * FROM orders WHERE user_id = ?", user_id)
# Business logic mixed with data access
# GOOD: Repository pattern
def calculate_discount(user: User, order_history: list[Order]) -> float:
# Pure business logic, easily testable
if len(order_history) > 10:
return 0.15
return 0.0Best Practices Summary
- Keep it simple - Choose the simplest solution that works
- Single responsibility - Each unit has one reason to change
- Separate concerns - Distinct layers with clear purposes
- Compose, don't inherit - Combine objects for flexibility
- Rule of three - Wait before abstracting
- Keep functions small - 20-50 lines (varies by complexity), one purpose
- Inject dependencies - Constructor injection for testability
- Delete before abstracting - Remove dead code, then consider patterns
- Test each layer - Isolated tests for each concern
- Explicit over clever - Readable code beats elegant code
Troubleshooting
A class is growing and seems to have multiple responsibilities, but splitting it feels wrong. Apply the "reason to change" test: list every change that could require editing this class. If the list has items from different domains (e.g., HTTP parsing AND business rules AND formatting), split it. If all changes stem from the same domain concern, the class may be appropriately sized.
Injecting all dependencies through the constructor is producing constructors with 7+ parameters. This is a sign of too many responsibilities in one class, not a problem with dependency injection. Split the class into smaller units first, then each constructor naturally becomes smaller.
Composition is producing deeply nested wrapper objects that are hard to trace. Keep the composition shallow (2-3 levels). If wrapping is the only mechanism, consider whether a Protocol-based approach or simple function composition would be cleaner than a chain of decorator objects.
The rule of three says not to abstract yet, but the duplication is causing bugs when one copy is updated but not the other. Duplication that diverges in dangerous ways should be abstracted sooner. The rule of three is a heuristic, not a law. If the copies are already diverging incorrectly, extract immediately and add a test that exercises the shared behavior.
A service layer is importing from the API layer, breaking the dependency direction. This is a layering violation. The service layer must not import from handlers. Introduce a shared types/models layer that both can import from, keeping the dependency arrow pointing downward (API → Service → Repository).
Related Skills
- python-testing-patterns — Test each layer in isolation using the dependency injection structure established here
- python-project-setup — Set up project structure and tooling that enforces layer boundaries from the start
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

