원클릭으로
python-idioms
Python type hints, Protocols, Pydantic, async/await, pytest, ruff, mypy strict.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Python type hints, Protocols, Pydantic, async/await, pytest, ruff, mypy strict.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
Structured fault tolerance for coordinator agents. 5-level escalation ladder (Retry → Replace → Skip → Redistribute → Degrade), dead-man timers, degraded completion protocol, and cross-level escalation format. Load when orchestrating agents that may fail.
Structured code review protocol for inspecting code quality against the full rule set. Use when auditing code written by yourself or another agent, during the /audit workflow, or when the user asks for a code review.
Reusable convergence protocol for coordinator agents. Defines the BRIEFING → ITERATE → GATE → CONVERGE loop, context hygiene rules, self-succession protocol, turn budget, and handoff compression. Load when orchestrating multi-iteration workflows.
Pre-flight checklist and post-implementation self-review protocol. Use before generating any code (pre-flight) and after writing code but before verification (self-review) to catch issues early.
MECE task decomposition, file ownership enforcement, DAG-based execution, and safe merge protocol for intra-domain parallel dispatch. The safety invariants that prevent merge chaos when multiple agents write in parallel. Applies recursively at every nesting depth.
Shared protocols for all agents in the multi-agent pipeline: recursive nesting, pre-implementation restatement, parallel dispatch format, and agent definition cascade. Load this skill instead of inlining these protocols in every agent file.
| name | python-idioms |
| description | Python type hints, Protocols, Pydantic, async/await, pytest, ruff, mypy strict. |
| paths | ["**/*.py","**/pyproject.toml","**/requirements*.txt"] |
Python rewards explicitness and readability over cleverness. Follow the Zen of Python (import this) — beautiful code is not a luxury, it's a professional necessity. If it reads like plain English, it's probably idiomatic Python.
Scope: This file covers Python-specific coding idioms. For file layout, see
references/project-structure.md. For test naming conventions, seetesting-strategy.md. For logging library choice, see @.agents/skills/logging-implementation/SKILL.md.
Always annotate function signatures and public APIs. Use from __future__ import annotations for forward references.
# ✅ Fully annotated — self-documenting and mypy-verifiable
from __future__ import annotations
from collections.abc import Sequence
def calculate_discount(items: Sequence[Item], coupon: Coupon) -> float: ...
# ❌ Untyped — opaque to both mypy and the next developer
def calculate_discount(items, coupon): ...
Use X | None over Optional[X] (Python 3.10+)
# ✅ Modern union syntax
def find_user(user_id: str) -> User | None: ...
# ❌ Verbose legacy form (still valid, but prefer the above)
from typing import Optional
def find_user(user_id: str) -> Optional[User]: ...
Use TypeAlias and TypeVar for reusable generics
from typing import TypeVar, TypeAlias
T = TypeVar("T")
UserId: TypeAlias = str
Use Protocol for structural interfaces instead of ABCs when duck-typing is sufficient
from typing import Protocol
class TaskStorage(Protocol):
def get_by_id(self, task_id: str) -> Task: ...
def save(self, task: Task) -> None: ...
TypedDict for structured dicts crossing system boundaries (JSON, configs)
from typing import TypedDict
class CreateTaskRequest(TypedDict):
title: str
priority: Literal["low", "medium", "high"]
For general error handling principles, see
error-handling-principles.md. This section covers Python-specific idioms.
Prefer specific exception types over broad except Exception
# ✅ Precise — catches exactly what you expect
try:
task = storage.get_by_id(task_id)
except TaskNotFoundError:
raise HTTPException(status_code=404, detail="Task not found")
# ❌ Broad — may swallow programming errors
try:
task = storage.get_by_id(task_id)
except Exception:
raise HTTPException(status_code=500, detail="Error")
Define domain-specific exception hierarchies
class FathError(Exception):
"""Base exception for all domain errors."""
class NotFoundError(FathError):
def __init__(self, resource: str, resource_id: str) -> None:
self.resource = resource
self.resource_id = resource_id
super().__init__(f"{resource} '{resource_id}' not found")
class ValidationError(FathError):
def __init__(self, field: str, message: str) -> None:
self.field = field
self.message = message
super().__init__(f"Validation failed on '{field}': {message}")
Never silence exceptions — if an exception is caught and not re-raised, log it explicitly
# ❌ Silent swallow
try:
notify_user(user_id)
except Exception:
pass
# ✅ Explicit intent + observability
try:
notify_user(user_id)
except NotificationError:
logger.warning("notification_failed", user_id=user_id, exc_info=True)
Use contextlib.suppress only for truly expected, inconsequential exceptions
from contextlib import suppress
with suppress(FileNotFoundError):
cache_path.unlink() # OK — cleanup, not business logic
Use dataclasses for internal domain models (no I/O, no validation)
from dataclasses import dataclass, field
@dataclass(frozen=True) # frozen = immutable value object
class Task:
id: str
title: str
priority: str
tags: tuple[str, ...] = field(default_factory=tuple)
Use Pydantic BaseModel for data crossing system boundaries (API requests/responses, config)
from pydantic import BaseModel, Field
class CreateTaskRequest(BaseModel):
title: str = Field(min_length=1, max_length=200)
priority: Literal["low", "medium", "high"] = "medium"
due_date: datetime | None = None
model_config = ConfigDict(frozen=True) # Pydantic v2
Keep domain models separate from API schemas — never use a Pydantic model as a domain entity
models.py → dataclasses (pure domain)
schemas.py → Pydantic models (API boundary)
Python uses Protocols and constructor injection to achieve the same testability goal as Go interfaces.
Define the Protocol where it is used, not where it is implemented
# task/storage.py ← defined in the consumer feature
from typing import Protocol
class TaskStorage(Protocol):
def get_by_id(self, task_id: str) -> Task: ...
def save(self, task: Task) -> None: ...
def delete(self, task_id: str) -> None: ...
Inject dependencies through __init__ — never instantiate concrete dependencies inside a class
# ✅ Testable — storage is injected
class TaskService:
def __init__(self, storage: TaskStorage) -> None:
self._storage = storage
# ❌ Not testable — concrete dependency hardwired
class TaskService:
def __init__(self) -> None:
self._storage = PostgresTaskStorage()
Wire dependencies in the entry point (e.g., main.py, app.py, or DI container)
# app/main.py
storage = PostgresTaskStorage(db=database)
service = TaskService(storage=storage)
router.include_router(build_task_router(service))
For general async principles (when to add concurrency), see
core-design-principles.md§ Concurrency. This section covers Python-specific async idioms.
Choose one async paradigm and stay consistent — do not mix asyncio.run entry points
# ✅ Fully async service layer
async def get_task(self, task_id: str) -> Task:
return await self._storage.get_by_id(task_id)
Never call blocking I/O directly in an async function
# ❌ Blocks the event loop
async def load_file(path: str) -> str:
return open(path).read()
# ✅ Use async I/O or run in executor
import asyncio, aiofiles
async def load_file(path: str) -> str:
async with aiofiles.open(path) as f:
return await f.read()
Use asyncio.gather for concurrent independent operations
# ✅ Concurrent fan-out
user, tasks = await asyncio.gather(
get_user(user_id),
get_tasks(user_id),
)
Use asyncio.TaskGroup (Python 3.11+) for structured concurrency with cancellation safety
async with asyncio.TaskGroup() as tg:
user_task = tg.create_task(get_user(user_id))
tasks_task = tg.create_task(get_tasks(user_id))
user = user_task.result()
tasks = tasks_task.result()
Follow PEP 8 rigorously. No exceptions.
| Construct | Convention | Example |
|---|---|---|
| Module / Package | snake_case | task_service.py |
| Class | PascalCase | TaskService |
| Function / Method | snake_case | get_by_id |
| Private method/attr | _snake_case | _validate_title |
| Constant | UPPER_SNAKE_CASE | MAX_TITLE_LENGTH = 200 |
| Type alias | PascalCase | UserId = str |
| Protocol / Interface | PascalCase | TaskStorage |
data, info, obj, result as standalone names — describe the domain concept# ✅
is_active: bool
has_permission: bool
def can_edit(user: User, task: Task) -> bool: ...
# ❌
active: bool
permission: bool
Context managers for resource cleanup — always prefer with over manual close()
# ✅
async with database.transaction() as tx:
await tx.execute(query)
# ❌
tx = database.begin()
tx.execute(query)
tx.commit() # easily forgotten on exception
Generator expressions over list comprehensions for lazy evaluation
# ✅ Lazy — does not materialise the entire list
active_ids = (task.id for task in tasks if task.is_active)
# Use list comprehension only when you need the full list
active_tasks = [task for task in tasks if task.is_active]
dataclasses.replace() for immutable updates (preferred over mutating frozen dataclasses)
from dataclasses import replace
updated_task = replace(task, title="New Title")
functools.cache / functools.lru_cache for pure function memoization
from functools import cache
@cache
def get_config() -> AppConfig: # called once; result reused
return _load_config_from_env()
__slots__ on hot-path, frequently instantiated classes
@dataclass
class Vector:
__slots__ = ("x", "y")
x: float
y: float
enum.Enum (not raw strings) for domain-level constants
from enum import StrEnum # Python 3.11+
class Priority(StrEnum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
Test file naming and pyramid proportions are defined in
testing-strategy.md. This section covers Python-specific tooling only.
Use pytest as the sole test runner — never mix with unittest.TestCase classes
# ✅ Idiomatic pytest
def test_calculate_discount_returns_zero_for_no_items() -> None:
result = calculate_discount(items=[], coupon=Coupon(code="SAVE10"))
assert result == 0.0
Parametrize test cases with @pytest.mark.parametrize
@pytest.mark.parametrize("priority,expected_score", [
("low", 1),
("medium", 5),
("high", 10),
])
def test_priority_score(priority: str, expected_score: int) -> None:
assert priority_score(priority) == expected_score
Use pytest-mock (mocker fixture) for mocking — not unittest.mock directly
def test_task_service_creates_task(mocker: MockerFixture) -> None:
mock_storage = mocker.create_autospec(TaskStorage, instance=True)
service = TaskService(storage=mock_storage)
service.create(title="Test", priority="high")
mock_storage.save.assert_called_once()
Use a typed mock factory for Protocol-based interfaces
# storage_mock.py ← co-locate with storage.py
class InMemoryTaskStorage:
"""In-memory TaskStorage implementation for unit tests."""
def __init__(self) -> None:
self._store: dict[str, Task] = {}
def get_by_id(self, task_id: str) -> Task:
if task_id not in self._store:
raise NotFoundError("Task", task_id)
return self._store[task_id]
def save(self, task: Task) -> None:
self._store[task.id] = task
def delete(self, task_id: str) -> None:
self._store.pop(task_id, None)
Use pytest-asyncio for async tests — mark the whole module or use asyncio_mode = "auto" in pyproject.toml
import pytest
@pytest.mark.asyncio
async def test_async_create_task() -> None:
service = TaskService(storage=InMemoryTaskStorage())
task = await service.create(title="Async Task", priority="low")
assert task.title == "Async Task"
Fixtures for reusable setup — never repeat identical Arrange blocks across tests
@pytest.fixture
def task_service() -> TaskService:
return TaskService(storage=InMemoryTaskStorage())
All of the following must pass with zero warnings/errors before any commit. See code-idioms-and-conventions.md for the full checklist.
| Tool | Purpose | Command |
|---|---|---|
ruff format | Canonical formatting (fast) | ruff format . |
ruff check | Lint (replaces flake8, isort, ...) | ruff check . --fix |
mypy | Static type checking | mypy src/ --strict |
bandit | Security scanning | bandit -r src/ -c pyproject.toml |
pip-audit | Dependency CVE scanning | pip-audit |
Configure all tools in pyproject.toml — never use per-file pragma comments to disable checks without a # NOQA: reason comment.
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "S", "B", "ANN"]
ignore = []
[tool.mypy]
strict = true
python_version = "3.11"
[tool.pytest.ini_options]
asyncio_mode = "auto"
Logging: Never use
print()in production code — it produces unstructured output. Use the standardloggingmodule orstructlogfor structured JSON logs. See @.agents/skills/logging-implementation/SKILL.md for the required patterns.