// Use this skill when building, reviewing, or refactoring Python code that requires strong maintainability discipline: SRP, DRY, OCP, explicit dependency injection, TDD/ATDD workflow, strict typing, architecture review, and clean project structure. Complements the project's Python CLAUDE.md with process rigor.
Use this skill when building, reviewing, or refactoring Python code that requires strong maintainability discipline: SRP, DRY, OCP, explicit dependency injection, TDD/ATDD workflow, strict typing, architecture review, and clean project structure. Complements the project's Python CLAUDE.md with process rigor.
Apply strict design and testing discipline for Python projects.
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.
<when_to_use>
Use this skill when:
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 Any types or tightening weak types in touched code
cleaning up hardcoded values or global mutable state
</when_to_use>
Follow this workflow:
Inspect the project first.
Read pyproject.toml, project layout, local CLAUDE.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.md or the relevant skill in the same change.
Verify locally.
Run formatting, linting, type checking, and tests appropriate to the affected package.
<design_rules>
Apply these rules during implementation:
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
</design_rules>
Use tmp_path for filesystem tests; time_machine or freezegun for 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.fixturedefclock() -> FakeClock:
return FakeClock(now=datetime(2026, 1, 1, tzinfo=UTC))
@pytest.fixturedeforder_service(clock: FakeClock, in_memory_store: InMemoryOrderStore) -> OrderService:
return OrderService(store=in_memory_store, clock=clock)
# sync test for sync service methodsdeftest_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.asyncioasyncdeftest_create_order_sets_timestamp(order_service: OrderService, clock: FakeClock) -> None:
order = await order_service.create(CreateOrderInput(product="widget"))
assert order.created_at == clock.now()
</testing_discipline>
<comment_rules>
Write comments only when they add information the code cannot carry cleanly on its own.
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
</comment_rules>
<error_and_type_rules>
Keep errors and types strict and readable.
Raise specific exceptions with context; never bare except: — it catches SystemExit and KeyboardInterrupt
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 specifically
raise NewError("context") from original_err to 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 ExceptionGroup and except* (3.11+) when multiple independent errors should be reported together
Return None or Optional only when absence is a valid, documented part of the contract
Do not use sentinel values when an exception or Optional return would be clearer
</error_and_type_rules>
<concurrency_rules>
See CLAUDE.md for the full concurrency rules. Apply this judgment discipline:
Require an explicit cancellation path before approving any new background task
Prefer asyncio.TaskGroup (3.11+) for structured concurrency over bare create_task; use asyncio.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.futures for simple parallelism; avoid raw threading unless necessary
Never nest asyncio.run() calls; one event loop per thread; prefer asyncio.run() or asyncio.Runner as the single entry point
</concurrency_rules>
<tooling_rules>
Verification is part of the implementation, not an optional cleanup step.
Run ruff format --check . (or ruff format . to fix)
Run ruff check . (with --fix for auto-fixable issues)
Run mypy . (strict mode)
Run pytest (with relevant markers or paths for the affected scope)
If dependencies changed, run uv lock and verify with uv sync
If public API changed, also run pytest --doctest-modules and verify docstrings
Treat linting and static analysis as normal development tools, not release-only checks
Fix root causes instead of scattering # noqa or # type: ignore comments
If a lint rule is intentionally suppressed, use the specific code and add a justification comment
</tooling_rules>
<review_checklist>
Before finishing, verify:
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.md and .claude/rules/ has been followed
</review_checklist>
<reject_patterns>
Reject these patterns:
giant functions mixing validation, orchestration, and persistence
Protocol-per-class abstraction without consumer need
hardcoded configuration or collaborator construction
Any added 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__.py files
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: ignore without 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
</reject_patterns>
<success_criteria>
This skill is being followed correctly when:
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.md and .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
</success_criteria>