This skill complements CLAUDE.md and .claude/rules/python-patterns.md. It must stay aligned with both and applies as an execution discipline layer on top of the project's Python standards.
This skill adds execution rigor:
- ATDD/TDD workflow
- SRP, DRY, and OCP decision rules
- explicit dependency injection discipline
- strict type discipline
- comment quality standards
- structured implementation and review checks
If CLAUDE.md is stricter on any point, follow CLAUDE.md.
- implementing a new feature or behavior increment
- refactoring Python code for clearer ownership or testability
- reviewing module boundaries or dependency flow
- replacing hidden collaborator construction with explicit injection
- tightening tests around user-visible or integration behavior
- removing
Anytypes or tightening weak types in touched code - cleaning up hardcoded values or global mutable state
Inspect the project first. Read
pyproject.toml, project layout, localCLAUDE.md, Makefile/justfile, tool configs (ruff.toml,mypy.ini,pytest.ini), and existing tests.Define acceptance behavior first. Express the user-visible outcome before writing implementation details.
Add or update an acceptance-level test when the project has that layer. Otherwise, write the closest boundary-level integration test.
Add the next smallest failing test. Prefer a focused unit or module test for the next behavior increment.
Implement the minimum change that makes the test pass. Keep the diff tight. Do not rewrite unrelated code.
Refactor while green. Improve naming, cohesion, dependency flow, and readability without changing behavior.
Keep standards in sync. If the task materially changes project conventions, architecture, or workflow expectations, update
CLAUDE.mdor the relevant skill in the same change.Verify locally. Run formatting, linting, type checking, and tests appropriate to the affected package.
- Keep project structure clean and predictable
- SRP: each module, class, and function should have one clear reason to change
- DRY: remove repeated validation, mapping, branching, and policy logic when the abstraction improves clarity
- OCP: extend behavior through composition, Protocols, configuration, and strategy injection instead of invasive branching or copy-paste forks
- Prefer domain-oriented module boundaries over technical dumping grounds
- Keep domain logic separate from transport, persistence, configuration, and presentation concerns
- Prefer the smallest coherent abstraction that solves the real duplication or extension point
- Do not introduce Protocol-first abstractions without real consumer pressure
- Prefer composition over inheritance; keep inheritance hierarchies shallow
- Use constructor injection (
__init__parameters) for long-lived collaborators - Use function parameters for short-lived collaborators and pure logic inputs
- Pass dependencies through typed config, constructors, or arguments
- Do not instantiate external clients, repositories, clocks, or runtime collaborators inside core domain logic
- Do not hardcode dependency selection, URLs, ports, credentials, or feature switches
- Avoid globals, module-level mutable state, and hidden singletons
Python-specific guidance:
- Prefer Protocols at dependency boundaries for structural typing
- Use
dataclassesor Pydantic models for typed configuration - pytest fixtures are the natural DI mechanism in tests — prefer fixtures over monkeypatch
- Use
functools.partialor closures for lightweight function-level injection - Avoid DI frameworks unless the project already uses one; explicit wiring is preferred
# constructor injection — domain logic stays testable
from dataclasses import dataclass
from typing import Protocol
class Clock(Protocol):
def now(self) -> datetime: ...
class OrderStore(Protocol):
async def save(self, order: Order) -> Order: ...
@dataclass(frozen=True, slots=True)
class OrderService:
store: OrderStore
clock: Clock
async def create(self, input: CreateOrderInput) -> Order:
now = self.clock.now() # no datetime.now() inside domain logic
return await self.store.save(Order(**vars(input), created_at=now))
# function parameter for short-lived/pure logic
def validate_order(order: Order, now: datetime) -> None:
if order.expires_at < now:
raise OrderExpiredError(order.id)
Keep the code easy to read and maintain.
- Write modern Python and prefer 3.12+ idioms in new code; see
.claude/rules/python-idioms.mdfor the full catalog - Keep functions short, explicit, and focused on one job
- Use consistent formatting and indentation; let
ruff formatdefine layout - Use whitespace to separate concepts, not decorate code
- Use meaningful whitespace: blank lines between logical sections within functions, two blank lines between top-level definitions
- Keep naming concrete and intention-revealing;
snake_casefor functions/variables,PascalCasefor classes - Prefer straightforward control flow over clever compression
- Early returns over deep nesting; guard clauses at the top
- Separate logic clearly so domain rules, I/O, transport, persistence, and orchestration are easy to trace
- Avoid hardcoded values; move runtime values and environment behavior into typed config, constants, or inputs
- Explicit imports; no
import *outside__init__.pyre-exports
mypy --strictis the baseline for all projects- Every public function has explicit parameter and return type annotations
- No
Anyin domain code; useobject,Protocol, generics,Union, or narrower types NewTypefor domain IDs and values that should not mix:UserId = NewType("UserId", int)— plain assignment, not thetypestatementProtocolfor dependency boundaries; enables structural typing without inheritance coupling@overrideon every overridden method (3.12+)TypeIsoverTypeGuardin new code (3.13)Literalfor constrained value sets;Finalfor immutable module-level bindings# type: ignore[code]requires a specific error code and inline justification — never bare# type: ignore- Avoid
cast()unless unavoidable; prefer narrowing withisinstance,TypeIs, or restructuring - Make invalid states unrepresentable with enums, dataclasses with validation, and constrained constructors
- Keep weakly typed data at the boundary and translate into strict internal types immediately
- ATDD first for user-visible changes: define the acceptance scenario before writing implementation
- Express the boundary behavior (Given/When/Then or equivalent scenario)
- Add or update the acceptance-level test
- Write the next smallest failing unit test
- Implement the minimum change that makes it pass
- Refactor while green
- Repeat for the next behavior increment
- TDD for the next increment: write the smallest failing unit test, implement, then refactor
- Use ATDD extensively for user-visible behavior, acceptance flows, and cross-boundary scenarios
- Every meaningful change should cover:
- expected behavior (happy path)
- invalid input and validation failures
- edge cases and boundary values
- error and failure paths
- Use
pytestwith fixtures; prefer fixtures over monkeypatch for dependency injection @pytest.mark.parametrizefor table-driven tests; direct narrative tests when variation is not the point- Fakes and stubs over mocks; test behavior not implementation
- Keep tests deterministic; avoid sleep-based flakiness
- Use
tmp_pathfor filesystem tests;time_machineorfreezegunfor time-dependent tests - Keep public API doctests accurate when behavior changes
- Separate test layers:
tests/unit/,tests/integration/,tests/acceptance/when the project uses that structure
# fixture-based DI in tests
@pytest.fixture
def clock() -> FakeClock:
return FakeClock(now=datetime(2026, 1, 1, tzinfo=UTC))
@pytest.fixture
def order_service(clock: FakeClock, in_memory_store: InMemoryOrderStore) -> OrderService:
return OrderService(store=in_memory_store, clock=clock)
# sync test for sync service methods
def test_validate_order_rejects_expired(clock: FakeClock) -> None:
order = Order(expires_at=clock.now() - timedelta(days=1))
with pytest.raises(OrderExpiredError):
validate_order(order, now=clock.now())
# async test using pytest-asyncio for async service methods
@pytest.mark.asyncio
async def test_create_order_sets_timestamp(order_service: OrderService, clock: FakeClock) -> None:
order = await order_service.create(CreateOrderInput(product="widget"))
assert order.created_at == clock.now()
Good comments explain:
- intent and rationale for non-obvious design choices
- invariants and constraints
- ownership or concurrency rules
- non-obvious tradeoffs or performance considerations
- why a particular approach was chosen over alternatives
Do not write comments that:
- restate the code
- narrate simple assignments or obvious operations
- explain standard Python syntax
- leave vague TODOs without context or ticket reference
- duplicate the docstring with less precision
Docstrings:
- Public API functions and classes need concise docstrings describing contract and behavior
- Use Google style or NumPy style consistently within a project; match existing convention
- Do not add docstrings to private helpers, test functions, or obvious one-liners
- Update docstrings when exported behavior, config, or API semantics change
- Raise specific exceptions with context; never bare
except:— it catchesSystemExitandKeyboardInterrupt except Exception:without re-raise is acceptable only at top-level boundary handlers (CLI entry points, web request handlers, task runners); everywhere else, re-raise or handle specificallyraise NewError("context") from original_errto preserve cause chains- Custom exception hierarchies for domain errors; stdlib exceptions for programming errors
contextlib.suppress(SpecificError)only for known-safe cases with clear justification- Keep error messages actionable and specific enough to debug
- Never log secrets, tokens, credentials, or sensitive payloads
- Use
ExceptionGroupandexcept*(3.11+) when multiple independent errors should be reported together - Return
NoneorOptionalonly when absence is a valid, documented part of the contract - Do not use sentinel values when an exception or
Optionalreturn would be clearer
- Require an explicit cancellation path before approving any new background task
- Prefer
asyncio.TaskGroup(3.11+) for structured concurrency over barecreate_task; useasyncio.gather(return_exceptions=True)when all tasks must complete regardless of failures - Flag shared mutable state across threads immediately; push toward immutable data or message passing
- Reject mixing sync and async I/O in the same code path without
run_in_executor - Use
concurrent.futuresfor simple parallelism; avoid rawthreadingunless necessary - Never nest
asyncio.run()calls; one event loop per thread; preferasyncio.run()orasyncio.Runneras the single entry point
- Run
ruff format --check .(orruff format .to fix) - Run
ruff check .(with--fixfor auto-fixable issues) - Run
mypy .(strict mode) - Run
pytest(with relevant markers or paths for the affected scope) - If dependencies changed, run
uv lockand verify withuv sync - If public API changed, also run
pytest --doctest-modulesand verify docstrings - Treat linting and static analysis as normal development tools, not release-only checks
- Fix root causes instead of scattering
# noqaor# type: ignorecomments - If a lint rule is intentionally suppressed, use the specific code and add a justification comment
- module boundaries are coherent — no cross-domain leaks
- responsibilities are not mixed across domain, transport, persistence, and config
- structure is clean, predictable, and free of dumping-ground modules
- dependencies are injected explicitly — no hidden construction or global state
- no hardcoded runtime values (URLs, ports, credentials, paths, timeouts)
- types are strict and explicit — no
Any, no bare# type: ignore - every public function has explicit parameter and return type annotations
- functions are short, focused, and readable in one pass
- formatting, indentation, and whitespace follow project conventions and
ruff - comments explain intent or invariants instead of restating the code
- error handling is clear, concise, contextual, and uses cause chains
- no bare
except:;except Exception:only at top-level boundaries; no swallowed errors; no mutable default arguments - tests cover acceptance behavior and unit behavior
- TDD/ATDD flow was followed as closely as the project constraints allowed
- async and concurrency behavior use structured patterns with cancellation paths
- public API docstrings are accurate and updated when behavior changed
- formatting, linting, type checking, and tests pass for the affected scope
- version/tooling guidance from
CLAUDE.mdand.claude/rules/has been followed
- giant functions mixing validation, orchestration, and persistence
- Protocol-per-class abstraction without consumer need
- hardcoded configuration or collaborator construction
Anyadded or left in touched code without explicit justification- bare
except:anywhere;except Exception:without re-raise outside top-level boundary handlers - mutable default arguments (
def fn(items=[])) import *in non-__init__.pyfiles- module-level mutable global state used as hidden dependency
- comments that restate code
- hidden singletons or global registries
- brittle mock-only tests when a fake or boundary test would be clearer
- transport or storage concerns embedded in core domain logic
# type: ignorewithout specific error code and justification- production design distorted to satisfy a mock framework
- large speculative refactors when a smaller coherent change would solve the task
cast()used to silence type errors instead of fixing the type- deep inheritance hierarchies when composition would be clearer
- changes are small, test-backed, and easy to review
- dependency flow is explicit from the composition root
- module responsibilities are cleaner after the change, not blurrier
- types in touched code are more precise after the change, not less
- the implementation matches the Python standards in
CLAUDE.mdand.claude/rules/ - tests speak in behavior terms, not implementation vocabulary
- the resulting code reads clearly without comments explaining the control flow
- the resulting code is easier to extend without rewriting stable behavior