Skip to content

docs(claude): add .claude/rules for Python code/review conventions#50

Merged
lorr1 merged 2 commits into
mainfrom
claude-rules-python
May 15, 2026
Merged

docs(claude): add .claude/rules for Python code/review conventions#50
lorr1 merged 2 commits into
mainfrom
claude-rules-python

Conversation

@lorr1
Copy link
Copy Markdown
Collaborator

@lorr1 lorr1 commented May 14, 2026

Adds four path-scoped rule files under .claude/rules/ that Claude Code auto-loads when touching matching Python files, plus a rules index in CLAUDE.md.

Summary

  • .claude/rules/python-style.md (**/*.py) — tooling (uv, ruff, ty), structlog logging, naming, docstrings, error handling, FastAPI + MCP patterns, security.
  • .claude/rules/python-types.md (**/*.py) — strict typing: no Any without a comment, no untyped dict / list, decision tree for pydantic.BaseModel / TypedDict / @dataclass, boundary parsing rule, str Enum for discriminators.
  • .claude/rules/python-testing.md (test files) — pytest layout, fixtures, parametrize-over-loops, mock at I/O boundaries only, no-network in unit tests, async patterns, FastAPI + MCP test patterns.
  • .claude/rules/test-quality.md (test files) — 11-rule rubric used by reviewers (human or agent) to evaluate test design and assertion strength.
  • CLAUDE.md — added a rules index pointing at the four files. Existing guidance kept.

How rules load

Each file has paths: frontmatter, so it only loads into Claude Code's context when files matching the glob are touched. This keeps the always-on CLAUDE.md short while letting detailed conventions live close to the code they govern.

Sources

Adapted from community-converged Python conventions for AI coding (May 2026) plus the curated rule set in claude-plugins:

Tailored to this repo's reality: Python 3.13, src/mcp_template_py/, FastAPI + MCP + Pydantic v2 + structlog, Taskfile commands (task lint / typecheck / test / security).

Test plan

  • Open a Python file in Claude Code and confirm python-style.md + python-types.md are visible in the rules context.
  • Open a test file and confirm python-testing.md + test-quality.md are visible.
  • Skim each rule file for accuracy against this repo (paths, commands, layout).
  • No code or CI behavior changes — task check should pass unchanged.

Adds four path-scoped rule files that auto-load when Claude Code touches
matching Python files, and updates CLAUDE.md to index them.

- python-style.md: tooling (uv/ruff/ty), structlog, naming, docstrings,
  error handling, FastAPI + MCP patterns, security.
- python-types.md: strict typing (no Any, no untyped dicts), decision
  tree for Pydantic/TypedDict/dataclass, boundary parsing, str Enum
  for discriminators.
- python-testing.md: pytest layout, fixtures, parametrize, mock at I/O
  boundaries only, no-network in unit tests, async patterns, FastAPI
  and MCP test patterns.
- test-quality.md: 11-rule rubric used by reviewers (human or agent)
  to evaluate test design and assertion strength.

Adapted from community-converged 2026 conventions and the
claude-plugins reference rule set, tailored to this repo (Python 3.13,
src/mcp_template_py/, FastAPI + MCP + Pydantic v2 + structlog).
@lorr1 lorr1 force-pushed the claude-rules-python branch from db39eb0 to 79c021e Compare May 14, 2026 23:32
@github-actions github-actions Bot added size/M and removed size/L labels May 14, 2026
Copy link
Copy Markdown
Collaborator

@glageju glageju left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-Agent Consensus Review

Agents consulted: docs-accuracy, consistency, python-correctness, docs-quality, codex (gpt-5.5)

Consensus Summary

# Finding Score Severity Action
F1 httpx.AsyncClient(app=app) example uses removed API 9/10 HIGH Fix
F4 # type: ignore[code] is mypy syntax; project uses ty 9/10 MEDIUM Fix
F5 Sync FastAPI handler claim is technically inaccurate 9/10 MEDIUM Fix
F10 Centralized test layout contradicts surviving CLAUDE.md text 9/10 MEDIUM Fix
F11 Template-repo: McpTemplateError / src/mcp_template_py/ baked in 9/10 MEDIUM Fix
F9 Stale task compose_agent_local in CLAUDE.md not fixed 8/10 MEDIUM Fix
F14 CLAUDE.md 'Technical considerations' duplicates new rule files 8/10 MEDIUM Fix
F15 **/*_test.py Go-style glob in two rule files 8/10 LOW Fix
F2 from __future__ import annotations mandate vs Pydantic on 3.13 7/10 MEDIUM Fix
F3 # any: <reason> is invented; phrased as if tool-enforced 7/10 MEDIUM Fix
F7 Test-layout block presents non-existent dirs as fact 7/10 MEDIUM Fix
F12 blocker: framing is self-contradictory in test-quality.md 7/10 MEDIUM Fix
F13 Rule 11 severity disagrees with python-types.md Hard Rule 1 7/10 MEDIUM Fix
F8 tests/helpers/ (Rule 4) doesn't exist 7/10 LOW Fix
F17 Frontmatter description asymmetry 7/10 LOW Fix
F19/F43 Cross-references use fragile rule numbers 7/10 LOW Fix
F22 structlog exc_info=True vs idiomatic log.exception(...) 7/10 LOW Fix
F23 Bandit suppression example uses uncommon form 7/10 LOW Fix
F24 MCP_AUTH_MODE fixture env var unverified 7/10 LOW Fix
F26 json.loads → dict[str, object] parenthetical incorrect 7/10 LOW Fix
F28 Async fixture scope ↔ asyncio_default_fixture_loop_scope not covered 7/10 LOW Fix

Overall

The approach — splitting strict typing, style, test-writing, and test-evaluation into four path-scoped files keyed on paths: globs — is sound and a strong fit for the CLAUDE.md context-budget problem. The findings below are about specific words, not the architecture.

Two findings rise above polish. F1 prescribes httpx.AsyncClient(app=app), which httpx 0.28 removed; agents and humans copying this pattern will hit a runtime error. F4 mandates # type: ignore[code] (mypy syntax) for suppressions, but the project uses ty, whose syntax is # ty: ignore[rule-name] — following the rule produces suppressions that don't actually suppress. Both are mechanical fixes worth landing before merge.

F11 is the longer-tail concern: this is a template repo, so hard-coded references to McpTemplateError and src/mcp_template_py/ will be wrong on day one in every downstream fork. Either abstract them or add a one-line note explaining they're template-specific placeholders.

Three findings (F9, F10, F14) are about CLAUDE.md leftovers the PR is already in a position to fix: a stale task compose_agent_local reference, a contradicted test-layout sentence, and duplicated uv/Taskfile bullets. The 'rule file wins' tiebreaker added in this PR papers over them rather than resolving them, and the project's own meta-CLAUDE explicitly warns against that pattern. See the inline comment on the rules-index area for specifics.

Recommendation: COMMENT — non-blocking, but several substantive fixes worth landing in this PR.

Dropped findings (auditability)

  • codex flagged: "pytest --cov claim conflicts with Taskfile (uv run pytest -v no --cov)". Dismissed by docs-accuracy reviewer with override NOTE(review): pyproject.toml has addopts = "--cov=mcp_template_py --cov-report html", so pytest's addopts auto-applies --cov to every invocation including the Taskfile's pytest -v. Rule claim is accurate.
  • mcp.shared.memory brittleness concern (raised by docs-accuracy + python-correctness): codex independently confirmed the path is current; downgraded.
  • ~22 additional cosmetic / defensible-house-style findings (full records preserved locally).

Generated with Claude Code

Comment thread .claude/rules/python-testing.md Outdated

## FastAPI tests

- `fastapi.testclient.TestClient` for sync style, `httpx.AsyncClient(app=app)` for async.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH · 9/10] httpx.AsyncClient(app=app) example uses removed API

app= was deprecated in httpx 0.27 and removed in httpx 0.28. Since this rule auto-loads on every test file, copy-pasters will hit a runtime error on a current httpx.

Raised by: python-correctness, docs-quality, codex

Suggested change
- `fastapi.testclient.TestClient` for sync style, `httpx.AsyncClient(app=app)` for async.
- `fastapi.testclient.TestClient` for sync style, `httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test")` for async.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4febeb3 — switched to httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test").

Comment thread .claude/rules/python-style.md Outdated

- stdlib / third-party / first-party, blank-line separated. ruff's `I` rule enforces it.
- Absolute imports inside the package; relative imports max one level deep. No wildcards.
- `from __future__ import annotations` at the top of every module.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM · 7/10] from __future__ import annotations mandate on 3.13

Most of this rule's motivation (PEP 604 unions, PEP 695 generics) is now in-language on 3.13, while deferred annotations make Pydantic v2 get_type_hints and FastAPI Depends resolution more fragile around forward refs in pydantic model and DI modules.

Consider softening to default-with-carveouts ('except in modules defining Pydantic models or FastAPI dependencies — those need eagerly-evaluated annotations'), or dropping the rule now that the floor is 3.13.

Raised by: docs-accuracy, python-correctness, docs-quality (codex argued the rule is safe-if-not-strictly-necessary; the Pydantic interaction risk remains real)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the rule entirely in 4febeb3.

Reasoning: with requires-python = ">=3.13", the original motivation (PEP 604 unions, PEP 695 generics) is in-language without from __future__ import annotations. The remaining benefit is faster startup from deferred evaluation, but in this stack the cost is real — Pydantic v2 get_type_hints and FastAPI Depends resolution both walk annotations eagerly and get flakier around forward refs when the module is from __future__-d. Net: more breakage than benefit on 3.13, so removing the mandate rather than carving out exceptions.

Comment thread .claude/rules/python-style.md Outdated
- Pydantic models for every request/response body. Never `dict` or `Any` — the OpenAPI schema is part of the contract.
- Dependencies via `Depends(...)`. `app.dependency_overrides[real] = fake` is the test seam.
- Explicit status codes (`status_code=201`); errors raise `HTTPException` with a specific code.
- Async handlers unless the work is genuinely CPU-bound — a sync handler in an async app blocks the worker.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM · 9/10] Sync FastAPI handler claim is technically inaccurate

FastAPI/Starlette runs def path operations in an anyio threadpool — they do not block the event loop or the worker. The real failure mode is threadpool saturation (default ~40 threads) and the thread-hop overhead. The current wording leads readers to 'fix' sync handlers for the wrong reason and miss the actual capacity ceiling.

Raised by: python-correctness, codex

Suggested change
- Async handlers unless the work is genuinely CPU-bound — a sync handler in an async app blocks the worker.
- Async handlers for I/O-bound work. FastAPI runs sync `def` handlers in a threadpool, which is fine for occasional CPU work but caps concurrency at the threadpool size and adds a thread hop — async handlers scale better for I/O. Use sync only when the work is genuinely CPU-bound or calls a sync-only library you can't wrap.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4febeb3 — reworded to name the actual failure mode (anyio threadpool ~40-worker cap on concurrent I/O-bound sync handlers), not "blocks the worker".

Comment thread .claude/rules/python-style.md Outdated

- No bare `except:` or `except BaseException:`. Catch the narrowest type that means something.
- Never silently swallow. Either log at WARNING with `exc_info=True` and comment why continuing is safe, or use `contextlib.suppress(SpecificError)` for genuinely-anticipated no-action exceptions.
- Define a small per-package exception hierarchy (`McpTemplateError` → narrower subclasses). No bare `Exception` / `RuntimeError`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM · 9/10] Template-repo: McpTemplateError will be wrong in every downstream fork

McpTemplateError does not exist in the current source tree and is package-name-specific. Because this is a TEMPLATE repo (users fork via 'Use this template' and rename), every fork will inherit a rule referencing a class that doesn't exist under its renamed package. Same concern for src/mcp_template_py/auth/ later in this file.

Either abstract the references (<PackageError>, src/<package_name>/) or add a top-level note marking template-specific names that fork users must rename.

Raised by: docs-accuracy, docs-quality, codex

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4febeb3 — abstracted to <PackageError> and src/<package_name>/ throughout python-style.md, plus a top-of-file template note explaining the placeholder convention. Forks now have one obvious thing to substitute rather than inheriting broken refs.

Comment thread .claude/rules/python-style.md Outdated
## Error handling

- No bare `except:` or `except BaseException:`. Catch the narrowest type that means something.
- Never silently swallow. Either log at WARNING with `exc_info=True` and comment why continuing is safe, or use `contextlib.suppress(SpecificError)` for genuinely-anticipated no-action exceptions.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW · 7/10] structlog exc_info=True works but log.exception(...) is more idiomatic inside except

Inside an except block, structlog's log.exception("event", ...) auto-attaches the active exception — cleaner than the stdlib-style exc_info=True. Either is fine; worth showing the preferred form since the rule mandates structlog two lines up.

Raised by: python-correctness

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4febeb3 — switched the error-handling guidance to prefer log.exception("event", ...) inside except, with contextlib.suppress as the alternative for anticipated no-action cases.

Comment thread .claude/rules/test-quality.md Outdated
### Rule 11 — Type-checked tests
Tests run through `ty`. Fixtures have return types. Mocks use `Mock(spec=Foo)` / `AsyncMock(spec=Foo)` so attribute typos fail at construction. No `Any`, no untyped fixture parameters.
- **❌ when:** the test file fails type checking, a fixture/test has untyped parameters or return type, or `Mock()` is constructed without `spec=`.
- **Severity:** `suggestion:` for missing return types on individual test functions; `blocker:` when the file fails type checking or constructs unspecced Mocks for the unit under test.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM · 7/10] Rule 11 severity disagrees with python-types.md Hard Rule 1

python-types.md Hard Rule 1 says 'Every parameter and return value is typed. No exceptions.' and Hard Rule 5 calls task typecheck a merge gate. Rule 11 here grades missing return types on test functions as only suggestion:. If task typecheck actually fails on untyped tests, the suggestion: severity is wrong — that's a merge-blocking failure, not a non-blocker.

Either tighten Rule 11 to blocker: for any missing type in a test file, or relax python-types.md Hard Rule 1 to scope test-function return types as advisory. The two files need to agree.

Raised by: consistency

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4febeb3 by tightening both sides:

  • python-types.md Hard Rule 1 is now scoped to non-test code; test-function return types are explicitly allowed to be omitted (test bodies do not return values, and pytest discovery does not require them).
  • Rule 11 here now blocks on untyped fixture parameters and unspecced Mocks for the unit under test; suggestion: is reserved for missing return-type annotations on individual test functions.

The two files now agree: missing test return types are a suggestion, everything else in test typing is a blocker.

Comment thread .claude/rules/test-quality.md Outdated
paths:
- "**/tests/**/*.py"
- "**/test_*.py"
- "**/*_test.py"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW · 8/10] **/*_test.py Go-style glob also dead here

Same issue as in python-testing.md frontmatter — _test.py is not pytest's discovery pattern, and nothing in this repo uses it. If removed in python-testing.md, remove it here too.

Raised by: docs-quality, codex

Suggested change
- "**/*_test.py"
- "**/tests/**/*.py"
- "**/test_*.py"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4febeb3 — dropped **/*_test.py from this file too.

Comment thread .claude/rules/test-quality.md Outdated
- "**/tests/**/*.py"
- "**/test_*.py"
- "**/*_test.py"
description: Test quality rubric — 11 rules reviewers apply to judge test design and assertion strength.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW · 7/10] Frontmatter description doesn't disclaim out-of-scope concerns

python-style.md's description says 'Type-strictness rules live in python-types.md.' python-testing.md's description says 'Quality rubric in test-quality.md.' These help readers orient when they land on one file. Symmetrically, this file could note 'Patterns for writing tests in python-testing.md', and python-types.md could note 'Style/tooling in python-style.md'.

Raised by: consistency

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4febeb3 — added symmetric "see also" pointers to all four rule files (python-style.md, python-types.md, python-testing.md, test-quality.md) so any landing point links to the other three.

Comment thread .claude/rules/python-types.md Outdated

## Tests are typed too

Test files run through `ty`. Fixtures have return types. Mocks declare what they're mocking — `Mock(spec=Foo)` / `AsyncMock(spec=Foo)`. See `test-quality.md` Rule 11.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW · 7/10] Cross-reference to 'Rule 11' is a fragile foreign key

If anyone adds, removes, or reorders rules in test-quality.md, this cross-reference rots silently. Use the rule's name or the heading anchor instead, e.g. see test-quality.md#rule-11--type-checked-tests. (Also worth back-referencing from test-quality.md Rule 11 to python-types.md — currently one-way.)

Raised by: consistency, docs-quality

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4febeb3 — replaced the bare "Rule 11" reference with a heading anchor (test-quality.md#rule-11--type-checked-tests). Also tightened Rule 11 itself to back-reference python-types.md Hard Rule 2 by name.

Comment thread CLAUDE.md
reviewers (human and agent) to evaluate test design. Loads for
test files.

When the rules below conflict with anything in `.claude/rules/`, the
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM · 8/10] 'Rule file wins' clause leaves real contradictions in CLAUDE.md unresolved

The clause is sound policy, but the document still contains three concrete conflicts the PR is in a position to just fix:

  1. F9 (8/10): existing CLAUDE.md references task compose_agent_local, which doesn't exist — the Taskfile defines task compose. No new rule covers deployment, so 'rule file wins' doesn't help. Fix to task compose directly.
  2. F10 (9/10): existing CLAUDE.md says 'Unit tests should be in a tests/ folder co-located with the source code being tested', directly contradicting python-testing.md's centralized layout. Delete the stale sentence rather than relying on the override clause.
  3. F14 (8/10): existing 'Technical considerations' bullets duplicate uv/Taskfile guidance now in python-style.md. Two sources of truth invite drift; the project's own meta-CLAUDE says 'don't leave a stale top-level doc for the next person'.

Raised by: docs-accuracy, consistency, docs-quality, codex

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed all three in 4febeb3 by editing CLAUDE.md directly rather than relying on the override clause:

  • F9: task compose_agent_localtask compose in the new Deployment section.
  • F10: Deleted the "Unit tests should be in a tests/ folder co-located with the source code" sentence.
  • F14: Removed the "Technical considerations" block. The uv / Taskfile / list vs List / uv run python -c guidance now lives only in python-style.md.

CLAUDE.md keeps the project blurb, rules index, deployment two-liner, code-structure note, and a tight implementation-guidelines block.

Fixes from multi-agent consensus review on PR #50:

- python-testing.md: replace removed httpx.AsyncClient(app=app) with
  ASGITransport form; drop Go-style **/*_test.py glob; mark scaffold
  dirs (top-level conftest, fixtures) as target layout; swap
  unverified MCP_AUTH_MODE fixture example for REQUIRE_BEARER_TOKEN;
  document asyncio_default_fixture_loop_scope footgun
- python-types.md: replace mypy # type: ignore[code] syntax with
  ty # ty: ignore[rule-name]; clarify # any: <reason> is a
  reviewer-enforced convention (not tool-enforced); fix json.loads
  typed as Any (not dict[str, object]); scope Hard Rule 1 to non-test
  code; anchor cross-ref to test-quality.md Rule 11; symmetric "see
  also" pointer to sibling rule files
- python-style.md: abstract template-specific names (<PackageError>,
  src/<package_name>/) so forks do not inherit broken refs; drop
  from __future__ import annotations mandate (PEP 604/695 unions are
  in-language on 3.13 and deferred annotations make Pydantic v2 model
  resolution flaky); reword sync FastAPI handler claim (threadpool
  saturation, not event-loop blocking); prefer log.exception over
  exc_info=True inside except; concrete bandit suppression example
- test-quality.md: drop Go-style _test.py glob; remove tests/helpers/
  reference (directory does not exist); resolve blocker: framing
  contradiction (blocker: now consistently merge-blocking); align
  Rule 11 severity with python-types.md (untyped fixture params are a
  blocker, missing test-function return types stay suggestion:);
  symmetric "see also" pointer
- CLAUDE.md: drop stale task compose_agent_local (Taskfile defines
  task compose); delete contradicted "co-located tests" sentence;
  remove uv/Taskfile bullets duplicated in python-style.md
@lorr1
Copy link
Copy Markdown
Collaborator Author

lorr1 commented May 15, 2026

Addressed all 21 consensus findings in 4febeb3 — in-thread replies on each. Summary of judgment calls:

  • F11 (template names): abstracted to <PackageError> and src/<package_name>/ with a template note up top, so forks substitute one obvious thing rather than inheriting broken refs.
  • F2 (from __future__ import annotations): dropped the rule entirely. On requires-python = ">=3.13" the original motivation is in-language, and deferred evaluation makes Pydantic v2 model resolution flaky. Net: more breakage than benefit, removed rather than carved-out.
  • F3 (# any: <reason>): kept the marker but tightened the framing — explicitly reviewer-enforced (not tool-enforced), and An unexplained Any is a blocker. Every Any requires an explanation; the comment is how reviewers see why.

Conflict-resolution batch (F9 / F10 / F14): edited CLAUDE.md directly instead of relying on the override clause — stale task compose_agent_local fixed, contradicted co-located-tests sentence deleted, duplicated uv/Taskfile bullets removed. The four rule files are now the single source of truth.

@lorr1 lorr1 merged commit 1d73d67 into main May 15, 2026
6 checks passed
@lorr1 lorr1 deleted the claude-rules-python branch May 15, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants