The same engineering rules as the Go plugin, written the way a Python developer would write them.
This Claude Code plugin is built from the same core as
go-linter-driven-development: twelve
design rules stated once as data, thin skills that sequence design, TDD, refactoring,
testing, review and documentation, and an evidence-based review run by fresh-context
agents. What differs is the language-shaped part: every canonical example is Python,
every detect line runs over .py files, the linter routing is keyed by ruff codes
and the type checker's (ty, or mypy), and the testing skill speaks pytest.
Install it for a Python repository. A repository in a language without a binding
takes linter-driven-development, which
detects the language at run time; a Go repository takes the Go plugin.
The rules are language-neutral. Where a Go idiom in a rule has no Python twin, this plugin takes a position, and the review hunts for the Python disease rather than the Go one:
Noneis a declared absence, never a disguised failure. A-> X | Nonereturn is fine when absence is normal and every caller narrows it —dict.getagainstdict[k]is the model. The findings areNonereturned where the signature promisesX,Nonestanding in for a failure (raise instead), and callers stackingis Noneguards because the absence should have been an exception.tuple[X, bool]is never the fix.- An optional collaborator is a Null Object constant.
NULL_SINK = NullSink()at module level, the parameter keyword-only and typedSink;Noneis rejected withTypeError. Asink: Sink | None = Nonedefault replaced inside__init__is allowed only for a genuinely mutable or expensive default, and even then no method guards the attribute. - A self-validating type is a frozen dataclass with
__post_init__, or aparseclassmethod that normalises and constructs. Public read-only fields are fine:__post_init__runs on every literal, so there is no path around it. The finding is a mutable dataclass carrying invariants with no__post_init__. Pydantic is the boundary form where the repository already uses it;model_constructandmodel_copy(update=)are the bypasses the hunter looks for; the plugin never proposes adding pydantic.NewTypescores zero on the invariant line. - The docstring summary line is the contract, exempt from the comment critic's
restatement verdict when it states that contract; the WHY budget applies to the
body;
Args:,Returns:andRaises:sections are free. Where the repository's ruffDrules require a docstring, a WHAT-docstring is rewritten, never deleted; on a_privatename it is deleted. - The kept switch is a
matchover anEnumclosed bycase _: assert_never(x). That arm is the completeness proof ty checks, not an unknown-kind default; acase _:that raises or logs is the finding. A dictionary of callables comes first, aProtocolhierarchy second,functools.singledispatchthird. A positional boolean parameter is always a finding (ruffFBT001) and the lint-fixer makes it keyword-only; a keyword-only boolean stays while the branches share their body. asyncio.sleepis cancellable by construction.time.sleepon a thread that has a stop condition is the finding, fixed withEvent.wait(timeout). The object that starts a thread exposesclose()that sets the event and joins; a daemon thread is acceptable only in entry-point wiring for work that owns no resource. asyncio tasks live in aTaskGroupor under a kept handle. There are no atomics and no race detector: aLockbeside the fields it guards, taken withwith, or confinement to one thread.- Module state has three lists. Silent everywhere:
logging.getLogger(__name__), constants, enums, frozen instances as constants, exception classes, typing machinery. Silent only in the entry point:Config.from_environ(),logging.basicConfig, the frameworkapp, a hand-filled registry,asyncio.run. Reported everywhere else:os.environreads and importedCONFIGobjects, module-level containers functions write into, side effects in a module body, a lazily built instance behind a getter,asyncio.runorget_event_loopin library code. A test that monkeypatches production configuration is evidence against the production code.
Some of what the plugin enforces is a stance, not a Python community norm. It holds them because the rules hold them in every language:
- No
utils.py, nocommon.py. A module is named for the domain vocabulary it holds, never for its role (R4, R5). - No testing of
_privatefunctions. The urge is a placement signal: the helper wants its own module or package, where its public API is testable (R4, R7). - No
mock.patchof internal collaborators. A patch on a class you own is a seam that exists only so a test can substitute a double — the same smell as a single-implementerProtocol. Patching the true external boundary — the clock, a socket, the process environment in an entry-point test — is fine (R6). - No mutable module state outside the entry point, and no
global(R8). - Small pytest fixtures that build a literal are fine; a fixture that hides the
input is not. A fixture earns its place for real infrastructure —
tmp_path, a fake server, a database — andconftest.pyholds only those (R7).
The plugin runs the checkers the repository already configures — ruff check,
ruff format, and ty check where a [tool.ty] table exists or mypy where a
[tool.mypy] table exists — through the task the repository defines. It never adds a
checker the repository does not use.
- Findings route by ruff code.
C901and thePLRcomplexity family go to R3,PLR0913to R1,PLW0603to R8,FBTto R11,B006/B008to R12. The mechanical row —ARG,SIM,RET,B904,BLE001,PLR2004, import order, style — is fixed by the lint-fixer in an isolated context. - Some findings have no ruff rule. Duplicated code, file length, a
matchmissing enum cases, aProtocolwith one implementer and shared-state races are review findings: the hunters own them alone. # noqa,# ty: ignoreand# type: ignoreare all suppressions. The lint-fixer never adds one and never edits[tool.ruff],[tool.ty]or[tool.mypy]; a new one in a diff is itself a review finding.
python-linter-driven-development/
├── maxims.md the uncompiled layer — named design questions above the rules
├── rules/ R1-primitive-obsession … R12-mutation-discipline (single source of truth)
├── skills/ linter-driven-development · code-designing · refactoring ·
│ pre-commit-review · testing · documentation (thin directional views)
├── agents/ rule-hunter · overabstraction-skeptic · comment-critic · lint-fixer
├── commands/ py-ldd-analyze · autopilot · quickfix · prepare · review · status · wire-repo-brain
├── examples/ storify-leaf-type · overabstraction-cidr · dependency-rejection ·
│ anti-if-dispatch · switch-to-polymorphism · private-comment-noise (case law, in Python)
└── scripts/ check-repo-brain.sh — repo-brain conformance gate with the Python adapter
ldd-scope.sh · ldd-detect.sh — the review's scope bundle and detection pass
rules/— R1–R12, each a self-contained hunter rulebook: Principle, Why, a canonical before/after in Python, Design guidance, a Fix pattern, and Falsifying questions, each with a detect line the detection script runs over.pyfiles — a grep or path pattern, an R9 gate question, orjudgment.skills/— thin directional views that sequence and route into the rules. They never restate rule content. The testing skill's reference is a short pytest harness catalogue.agents/— read-only or mechanical workers spawned in isolated contexts, pointed by path at the relevant rule files — a hunter gets its rule family's, four hunters at most — and, for hunters and the critic, at the scope bundle the review scripts wrote — a hunter gets its family's rows of the detection pass's hits table, the critic the bundle's comment lines — which they read in their first turn.scripts/check-repo-brain.sh— the repo-brain gate. Its Python adapter resolves backticked symbols against classes, functions, methods and module-level assignments, and treats apyproject.tomldirectory as a sub-project with its own doc root.examples/— the worked case studies the rules cite, in Python: the frozen dataclass that beat a wrapper, theIPConfigleaf a flag-driven loop became, the alert channels dispatched through aProtocoland amatchclosed byassert_never, the export patches that fill their own request,env.CONFIGpushed up to the app factory, and the nine_privatedocstrings judged one by one. The verdicts and decision questions are the same as the Go plugin's; the code is this plugin's.
The @linter-driven-development skill
is the meta-orchestrator:
1 DESIGN @code-designing → DESIGN PLAN → user OK
1.5 PREPARE preparatory refactoring: survey the plan's touch points,
four autonomous gates decide, @refactoring reshapes → prep commit(s)
2 IMPLEMENT per behavior: RED (one failing pytest, lowest rung) → GREEN → REFACTOR
3 FULL LINT ONE run of ruff and the type checker via the lint-fixer agent (isolated context)
mechanical → FIXED · design → ESCALATED → @refactoring
4 REVIEW per completed slice: @pre-commit-review spawns hunters + skeptic + critic
5 SHIP @documentation → commit (tests and lint green, tree dirty) → ship summary
| Command | Purpose | Auto-Fix |
|---|---|---|
/py-ldd-autopilot [request] |
Full workflow (Phases 1–5) | ✅ Yes |
/py-ldd-quickfix [files | --all] |
Quality-gates loop until green over the files you are working on | ✅ Yes |
/py-ldd-prepare <change> [files] |
Preparatory refactoring ahead of a planned change | ✅ Yes |
/py-ldd-analyze [files | --all] |
Tests + lint + review, combined report | ❌ No |
/py-ldd-review [files | --all] |
Commit-readiness check | ❌ No |
/py-ldd-status |
Show current phase + progress | N/A |
/wire-repo-brain [path] |
Wire the documentation network in one pass | ✅ Wiring only |
/wire-repo-brain carries no prefix: the documentation network it wires is a
property of the repository, not of a language, and the Go plugin offers the same
command. Installed side by side, both do the same structural work.
/plugin marketplace add buzzdan/ai-coding-rules
/plugin install python-linter-driven-development@ai-coding-rules
Verify with /plugin list; it should show python-linter-driven-development (enabled).
First steps: docs/quick-start.md — the scenario guide, easiest start first.
When you request code work ("implement feature X", "fix the bug in the handler") in a Python project, Claude detects that the skill applies and asks for permission. Select "don't ask again for this skill in this directory" on first use.
Triggers auto-detection: action verbs (implement, fix, build, add,
refactor, update, change, modify) in a Python project, a mention of "ldd" or
"@ldd", or a /py-ldd-* command.
/plugin update python-linter-driven-development@ai-coding-rules
See CHANGELOG.md for what changed between versions. To uninstall,
run /plugin, select "python-linter-driven-development" and choose "Uninstall".
Full details, the generator that renders this plugin from its core, and the Go and generic plugins it shares that core with: the main repository. Found a bug or have an idea? Open an issue.
Apache-2.0. Use it anywhere, for any code, commercial or not. The code you write with this plugin is yours.