modern-python-guidance 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/CHANGELOG.md +13 -0
  2. modern_python_guidance-0.2.1/CONTRIBUTING.md +49 -0
  3. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/PKG-INFO +48 -100
  4. modern_python_guidance-0.2.1/README.md +134 -0
  5. modern_python_guidance-0.2.1/bench/fixtures/variant-a-modern/pyproject.toml +16 -0
  6. modern_python_guidance-0.2.1/bench/fixtures/variant-a-modern/src/app.py +36 -0
  7. modern_python_guidance-0.2.1/bench/fixtures/variant-a-modern/src/config.py +20 -0
  8. modern_python_guidance-0.2.1/bench/fixtures/variant-a-modern/src/crawler.py +24 -0
  9. modern_python_guidance-0.2.1/bench/fixtures/variant-a-modern/src/models.py +71 -0
  10. modern_python_guidance-0.2.1/bench/fixtures/variant-a-modern/src/scanner.py +42 -0
  11. modern_python_guidance-0.2.1/bench/fixtures/variant-a-modern/src/utils.py +17 -0
  12. modern_python_guidance-0.2.1/bench/fixtures/variant-a-outdated/pyproject.toml +19 -0
  13. modern_python_guidance-0.2.1/bench/fixtures/variant-a-outdated/setup.py +7 -0
  14. modern_python_guidance-0.2.1/bench/fixtures/variant-a-outdated/src/app.py +32 -0
  15. modern_python_guidance-0.2.1/bench/fixtures/variant-a-outdated/src/config.py +23 -0
  16. modern_python_guidance-0.2.1/bench/fixtures/variant-a-outdated/src/crawler.py +36 -0
  17. modern_python_guidance-0.2.1/bench/fixtures/variant-a-outdated/src/models.py +62 -0
  18. modern_python_guidance-0.2.1/bench/fixtures/variant-a-outdated/src/scanner.py +37 -0
  19. modern_python_guidance-0.2.1/bench/fixtures/variant-a-outdated/src/utils.py +30 -0
  20. modern_python_guidance-0.2.1/bench/fixtures/variant-b-modern/myapp/models.py +15 -0
  21. modern_python_guidance-0.2.1/bench/fixtures/variant-b-modern/myapp/views.py +14 -0
  22. modern_python_guidance-0.2.1/bench/fixtures/variant-b-outdated/myapp/models.py +16 -0
  23. modern_python_guidance-0.2.1/bench/fixtures/variant-b-outdated/myapp/views.py +18 -0
  24. modern_python_guidance-0.2.1/bench/fixtures/variant-c-modern/tests/test_calculator.py +37 -0
  25. modern_python_guidance-0.2.1/bench/fixtures/variant-c-outdated/tests/test_calculator.py +35 -0
  26. modern_python_guidance-0.2.1/bench/mcp-config.json +8 -0
  27. modern_python_guidance-0.2.1/bench/prompt-v3-mcp.txt +20 -0
  28. modern_python_guidance-0.2.1/bench/prompt-v4-a.txt +15 -0
  29. modern_python_guidance-0.2.1/bench/prompt-v4-b.txt +7 -0
  30. modern_python_guidance-0.2.1/bench/prompt-v4-c.txt +3 -0
  31. modern_python_guidance-0.2.1/bench/run-mcp.sh +345 -0
  32. modern_python_guidance-0.2.1/bench/run-v4.sh +191 -0
  33. modern_python_guidance-0.2.1/bench/score-v4.sh +803 -0
  34. modern_python_guidance-0.2.1/bench/test-scorer.sh +94 -0
  35. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/docs/benchmark-evaluation.md +432 -0
  36. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/pyproject.toml +1 -1
  37. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/src/modern_python_guidance/mcp_server.py +12 -29
  38. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/tests/test_mcp_server.py +6 -19
  39. modern_python_guidance-0.2.0/README.md +0 -186
  40. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/.github/workflows/ci.yml +0 -0
  41. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/.github/workflows/publish.yml +0 -0
  42. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/.gitignore +0 -0
  43. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/LICENSE +0 -0
  44. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/LICENSE-MIT +0 -0
  45. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/SECURITY.md +0 -0
  46. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/bench/prompt-v2.txt +0 -0
  47. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/bench/prompt-v3.txt +0 -0
  48. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/bench/prompt.txt +0 -0
  49. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/bench/run.sh +0 -0
  50. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/bench/score-v2.sh +0 -0
  51. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/bench/score-v3.sh +0 -0
  52. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/bench/score.sh +0 -0
  53. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/docs/benchmark-procedure.md +0 -0
  54. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/docs/design.md +0 -0
  55. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/SKILL.md +0 -0
  56. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/async/async-timeout-context.md +0 -0
  57. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/async/exception-groups.md +0 -0
  58. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +0 -0
  59. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +0 -0
  60. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +0 -0
  61. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +0 -0
  62. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/django/django-async-views.md +0 -0
  63. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/django/django-check-constraints.md +0 -0
  64. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/django/django-json-field.md +0 -0
  65. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +0 -0
  66. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +0 -0
  67. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +0 -0
  68. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +0 -0
  69. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +0 -0
  70. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +0 -0
  71. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +0 -0
  72. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +0 -0
  73. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +0 -0
  74. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/pytest/pytest-parametrize.md +0 -0
  75. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/pytest/pytest-raises-match.md +0 -0
  76. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/pytest/pytest-tmp-path.md +0 -0
  77. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-2-style.md +0 -0
  78. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-async-session.md +0 -0
  79. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/sqlalchemy/sqlalchemy-mapped-column.md +0 -0
  80. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +0 -0
  81. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +0 -0
  82. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +0 -0
  83. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +0 -0
  84. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/toolchain/no-pickle.md +0 -0
  85. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +0 -0
  86. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +0 -0
  87. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +0 -0
  88. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +0 -0
  89. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/typing/override-decorator.md +0 -0
  90. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +0 -0
  91. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +0 -0
  92. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +0 -0
  93. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/typing/union-syntax.md +0 -0
  94. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +0 -0
  95. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/src/modern_python_guidance/__init__.py +0 -0
  96. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/src/modern_python_guidance/__main__.py +0 -0
  97. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/src/modern_python_guidance/cli.py +0 -0
  98. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/src/modern_python_guidance/compat.py +0 -0
  99. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/src/modern_python_guidance/frontmatter.py +0 -0
  100. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/src/modern_python_guidance/guide_index.py +0 -0
  101. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/src/modern_python_guidance/retrieve.py +0 -0
  102. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/src/modern_python_guidance/search.py +0 -0
  103. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/src/modern_python_guidance/version_detect.py +0 -0
  104. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/tests/test_cli_integration.py +0 -0
  105. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/tests/test_frontmatter.py +0 -0
  106. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/tests/test_retrieve.py +0 -0
  107. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/tests/test_search.py +0 -0
  108. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/tests/test_skill_sync.py +0 -0
  109. {modern_python_guidance-0.2.0 → modern_python_guidance-0.2.1}/tests/test_version_detect.py +0 -0
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.1] — 2026-05-27
6
+
7
+ ### Changed
8
+
9
+ - README rewrite: benefit-framed tagline, benchmark highlights, MCP-first quick start, persona-routed delivery methods
10
+ - Moved project structure and guide authoring spec from README to CONTRIBUTING.md
11
+ - Development section condensed to 5 lines + link
12
+
13
+ ### Added
14
+
15
+ - CONTRIBUTING.md with project structure, guide authoring spec, and test instructions
16
+ - Benchmark results (+21.9pp) featured in README highlights
17
+
5
18
  ## [0.2.0] — 2026-05-27
6
19
 
7
20
  ### Added
@@ -0,0 +1,49 @@
1
+ # Contributing
2
+
3
+ ## Project structure
4
+
5
+ ```
6
+ src/modern_python_guidance/
7
+ ├── cli.py # Entry point (search, retrieve, list, detect-version, mcp)
8
+ ├── mcp_server.py # MCP server (JSON-RPC 2.0 over stdio)
9
+ ├── frontmatter.py # YAML-subset parser (no PyYAML dependency)
10
+ ├── guide_index.py # Guide discovery and indexing
11
+ ├── search.py # Weighted keyword search + fuzzy fallback
12
+ ├── retrieve.py # Guide retrieval and JSON rendering
13
+ ├── version_detect.py # Python version auto-detection
14
+ └── compat.py # Shared helpers
15
+
16
+ skills/modern-python-guidance/
17
+ ├── SKILL.md # Agent Skills plugin entry point
18
+ └── guides/ # 39 guide files by category
19
+ ```
20
+
21
+ See [docs/design.md](docs/design.md) for the full design document.
22
+
23
+ ## Adding a new guide
24
+
25
+ 1. Create `skills/modern-python-guidance/guides/<category>/<id>.md`
26
+ 2. Include YAML frontmatter with these fields:
27
+
28
+ | Field | Type | Values |
29
+ |-------|------|--------|
30
+ | `id` | string | Unique kebab-case identifier (must match filename) |
31
+ | `title` | string | Short descriptive title |
32
+ | `category` | string | Must match parent directory name |
33
+ | `layer` | int | 1 (stdlib), 2 (frameworks), 3 (toolchain) |
34
+ | `tags` | list | Search keywords |
35
+ | `aliases` | list | Alternate names (old API names, etc.) |
36
+ | `python` | string | Minimum version, e.g. `">=3.11"` |
37
+ | `frequency` | string | `high` (LLMs do this often), `medium`, `low` |
38
+
39
+ 3. Write BAD/GOOD/Why/Version Notes sections
40
+ 4. Run `pytest` to verify the guide parses correctly
41
+
42
+ ## Running tests
43
+
44
+ ```bash
45
+ uv venv && source .venv/bin/activate
46
+ uv pip install -e ".[dev]"
47
+ pytest
48
+ ruff check src/ tests/
49
+ ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modern-python-guidance
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Version-aware BAD/GOOD pattern guides that help AI coding agents generate modern Python
5
5
  Project-URL: Homepage, https://github.com/yottayoshida/modern-python-guidance
6
6
  Project-URL: Repository, https://github.com/yottayoshida/modern-python-guidance
@@ -36,29 +36,65 @@ Description-Content-Type: text/markdown
36
36
  [![Python](https://img.shields.io/pypi/pyversions/modern-python-guidance.svg)](https://pypi.org/project/modern-python-guidance/)
37
37
  [![License](https://img.shields.io/github/license/yottayoshida/modern-python-guidance.svg)](LICENSE)
38
38
 
39
- LLMs often produce outdated Python `typing.List` instead of `list`, `@validator` instead of `@field_validator`, `setup.py` instead of `pyproject.toml`. This tool provides 39 version-aware BAD/GOOD pattern guides that show the modern replacement, filtered by your project's Python version.
39
+ Stop your AI from writing `typing.List`, `@validator`, and `setup.py`. 39 version-aware BAD/GOOD pattern guides that teach AI coding agents to write modern Python delivered via MCP, CLI, or Agent Skills.
40
+
41
+ ## Highlights
42
+
43
+ - **Measurable impact**: +14.7pp overall improvement in A/B benchmark with 38 scored items ([details](docs/benchmark-evaluation.md)). Largest variant (FastAPI, 32 items): Control 60.4% → Treatment 82.3%
44
+ - **39 guides** across stdlib, Pydantic, FastAPI, Django, SQLAlchemy, pytest, and toolchain
45
+ - **Version-aware**: auto-detects your project's Python version and filters guides accordingly
46
+ - **3 delivery methods**: MCP server, CLI, Agent Skills plugin
47
+ - **Not Ruff**: Ruff auto-fixes syntax (`List` → `list`). mpg guides design decisions that Ruff can't touch — `TaskGroup` over `gather`, Pydantic V2 migration, SQLAlchemy 2.0 style
40
48
 
41
49
  > **Note:** The tool itself requires Python 3.11+ to run. Guides cover patterns from Python 3.9 onward, and `--python-version` filters guides for your target environment.
42
50
 
43
51
  ## Quick start
44
52
 
53
+ ### MCP (for AI coding agents)
54
+
55
+ Install, then register the MCP server with your agent:
56
+
57
+ ```bash
58
+ pip install modern-python-guidance
59
+ ```
60
+
61
+ **Claude Code:**
62
+ ```bash
63
+ claude mcp add mpg -- mpg mcp
64
+ ```
65
+
66
+ **Other MCP-compatible agents** (Cursor, Windsurf, etc.) — add to your MCP config:
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "mpg": {
71
+ "command": "mpg",
72
+ "args": ["mcp"]
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ Your agent gets access to `search_guides`, `retrieve_guides`, `list_guides`, and `detect_python_version`.
79
+
80
+ ### CLI
81
+
45
82
  ```bash
46
- # Install
47
83
  pip install modern-python-guidance
48
84
 
49
85
  # Search for a pattern
50
86
  mpg search "pydantic validator"
51
- # pydantic-v2-validators score=18.0 [pydantic]
52
87
 
53
88
  # Get the full guide
54
89
  mpg retrieve pydantic-v2-validators
55
- # --- pydantic-v2-validators (version match: YES) ---
56
- # ## BAD
57
- # @validator("name")
58
- # ...
59
- # ## GOOD
60
- # @field_validator("name")
61
- # ...
90
+ ```
91
+
92
+ ### Agent Skills (Claude Code plugin)
93
+
94
+ ```bash
95
+ # Symlink into your project
96
+ SKILL_DIR=$(python -c "from pathlib import Path; import modern_python_guidance; print(Path(modern_python_guidance.__file__).parent / 'skills' / 'modern-python-guidance')")
97
+ ln -s "$SKILL_DIR" your-project/.claude/skills/modern-python-guidance
62
98
  ```
63
99
 
64
100
  `mpg` is the short alias for `modern-python-guidance`. Both work.
@@ -112,54 +148,6 @@ mpg list --python-version 3.9
112
148
  # Excludes: TaskGroup (3.11+), match/case (3.10+), etc.
113
149
  ```
114
150
 
115
- ## MCP server
116
-
117
- mpg includes a built-in [MCP](https://modelcontextprotocol.io) server that exposes all 4 commands as tools. AI agents (Claude Code, Cursor, Gemini CLI, etc.) can discover and call them directly.
118
-
119
- ### Setup with Claude Code
120
-
121
- ```bash
122
- claude mcp add mpg -- mpg mcp
123
- ```
124
-
125
- Or add to `.mcp.json` manually:
126
-
127
- ```json
128
- {
129
- "mcpServers": {
130
- "mpg": {
131
- "command": "mpg",
132
- "args": ["mcp"]
133
- }
134
- }
135
- }
136
- ```
137
-
138
- ### Available tools
139
-
140
- | Tool | Description |
141
- |------|-------------|
142
- | `search_guides` | Search guides by keyword with fuzzy matching |
143
- | `retrieve_guides` | Get full BAD/GOOD content by guide ID |
144
- | `list_guides` | Browse all guides, filter by category/version |
145
- | `detect_python_version` | Auto-detect project Python version |
146
-
147
- The MCP server uses stdio transport (JSON-RPC 2.0) and adds zero additional dependencies.
148
-
149
- ## Agent Skills integration
150
-
151
- This project doubles as a [Claude Code Agent Skills](https://docs.anthropic.com/en/docs/claude-code) plugin. Install it into your project's `.claude/skills/` to give Claude automatic access to modern Python patterns when writing or reviewing code.
152
-
153
- ```bash
154
- # Find where the package is installed
155
- SKILL_DIR=$(python -c "from pathlib import Path; import modern_python_guidance; print(Path(modern_python_guidance.__file__).parent / 'skills' / 'modern-python-guidance')")
156
-
157
- # Symlink into your project
158
- ln -s "$SKILL_DIR" your-project/.claude/skills/modern-python-guidance
159
- ```
160
-
161
- For other AI tools (Cursor, Copilot, etc.), use the CLI directly — pipe `mpg search` or `mpg retrieve` output into your workflow.
162
-
163
151
  ## Development
164
152
 
165
153
  ```bash
@@ -168,49 +156,9 @@ cd modern-python-guidance
168
156
  uv venv && source .venv/bin/activate
169
157
  uv pip install -e ".[dev]"
170
158
  pytest
171
- ruff check src/ tests/
172
159
  ```
173
160
 
174
- ### Project structure
175
-
176
- ```
177
- src/modern_python_guidance/
178
- ├── cli.py # Entry point (search, retrieve, list, detect-version, mcp)
179
- ├── mcp_server.py # MCP server (JSON-RPC 2.0 over stdio)
180
- ├── frontmatter.py # YAML-subset parser (no PyYAML dependency)
181
- ├── guide_index.py # Guide discovery and indexing
182
- ├── search.py # Weighted keyword search + fuzzy fallback
183
- ├── retrieve.py # Guide retrieval and JSON rendering
184
- ├── version_detect.py # Python version auto-detection
185
- └── compat.py # Shared helpers
186
-
187
- skills/modern-python-guidance/
188
- ├── SKILL.md # Agent Skills plugin entry point
189
- └── guides/ # 39 guide files by category
190
- ```
191
-
192
- See [docs/design.md](docs/design.md) for the full design document.
193
-
194
- ## Contributing
195
-
196
- Contributions welcome! To add a new guide:
197
-
198
- 1. Create `skills/modern-python-guidance/guides/<category>/<id>.md`
199
- 2. Include YAML frontmatter with these fields:
200
-
201
- | Field | Type | Values |
202
- |-------|------|--------|
203
- | `id` | string | Unique kebab-case identifier (must match filename) |
204
- | `title` | string | Short descriptive title |
205
- | `category` | string | Must match parent directory name |
206
- | `layer` | int | 1 (stdlib), 2 (frameworks), 3 (toolchain) |
207
- | `tags` | list | Search keywords |
208
- | `aliases` | list | Alternate names (old API names, etc.) |
209
- | `python` | string | Minimum version, e.g. `">=3.11"` |
210
- | `frequency` | string | `high` (LLMs do this often), `medium`, `low` |
211
-
212
- 3. Write BAD/GOOD/Why/Version Notes sections
213
- 4. Run `pytest` to verify the guide parses correctly
161
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for project structure and guide authoring details.
214
162
 
215
163
  ## License
216
164
 
@@ -0,0 +1,134 @@
1
+ # modern-python-guidance
2
+
3
+ [![CI](https://github.com/yottayoshida/modern-python-guidance/actions/workflows/ci.yml/badge.svg)](https://github.com/yottayoshida/modern-python-guidance/actions/workflows/ci.yml)
4
+ [![PyPI version](https://img.shields.io/pypi/v/modern-python-guidance.svg)](https://pypi.org/project/modern-python-guidance/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/modern-python-guidance.svg)](https://pypi.org/project/modern-python-guidance/)
6
+ [![License](https://img.shields.io/github/license/yottayoshida/modern-python-guidance.svg)](LICENSE)
7
+
8
+ Stop your AI from writing `typing.List`, `@validator`, and `setup.py`. 39 version-aware BAD/GOOD pattern guides that teach AI coding agents to write modern Python — delivered via MCP, CLI, or Agent Skills.
9
+
10
+ ## Highlights
11
+
12
+ - **Measurable impact**: +14.7pp overall improvement in A/B benchmark with 38 scored items ([details](docs/benchmark-evaluation.md)). Largest variant (FastAPI, 32 items): Control 60.4% → Treatment 82.3%
13
+ - **39 guides** across stdlib, Pydantic, FastAPI, Django, SQLAlchemy, pytest, and toolchain
14
+ - **Version-aware**: auto-detects your project's Python version and filters guides accordingly
15
+ - **3 delivery methods**: MCP server, CLI, Agent Skills plugin
16
+ - **Not Ruff**: Ruff auto-fixes syntax (`List` → `list`). mpg guides design decisions that Ruff can't touch — `TaskGroup` over `gather`, Pydantic V2 migration, SQLAlchemy 2.0 style
17
+
18
+ > **Note:** The tool itself requires Python 3.11+ to run. Guides cover patterns from Python 3.9 onward, and `--python-version` filters guides for your target environment.
19
+
20
+ ## Quick start
21
+
22
+ ### MCP (for AI coding agents)
23
+
24
+ Install, then register the MCP server with your agent:
25
+
26
+ ```bash
27
+ pip install modern-python-guidance
28
+ ```
29
+
30
+ **Claude Code:**
31
+ ```bash
32
+ claude mcp add mpg -- mpg mcp
33
+ ```
34
+
35
+ **Other MCP-compatible agents** (Cursor, Windsurf, etc.) — add to your MCP config:
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "mpg": {
40
+ "command": "mpg",
41
+ "args": ["mcp"]
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ Your agent gets access to `search_guides`, `retrieve_guides`, `list_guides`, and `detect_python_version`.
48
+
49
+ ### CLI
50
+
51
+ ```bash
52
+ pip install modern-python-guidance
53
+
54
+ # Search for a pattern
55
+ mpg search "pydantic validator"
56
+
57
+ # Get the full guide
58
+ mpg retrieve pydantic-v2-validators
59
+ ```
60
+
61
+ ### Agent Skills (Claude Code plugin)
62
+
63
+ ```bash
64
+ # Symlink into your project
65
+ SKILL_DIR=$(python -c "from pathlib import Path; import modern_python_guidance; print(Path(modern_python_guidance.__file__).parent / 'skills' / 'modern-python-guidance')")
66
+ ln -s "$SKILL_DIR" your-project/.claude/skills/modern-python-guidance
67
+ ```
68
+
69
+ `mpg` is the short alias for `modern-python-guidance`. Both work.
70
+
71
+ ## CLI usage
72
+
73
+ ```bash
74
+ # Search guides by keyword
75
+ mpg search "pydantic validator"
76
+
77
+ # Retrieve a specific guide (full BAD/GOOD content)
78
+ mpg retrieve use-builtin-generics
79
+
80
+ # List all guides compatible with your Python version
81
+ mpg list --python-version 3.11
82
+
83
+ # Auto-detect project Python version from pyproject.toml / .python-version
84
+ mpg detect-version
85
+
86
+ # Filter by category
87
+ mpg search "timeout" --category async
88
+
89
+ # JSON output (default when piped, explicit with --format)
90
+ mpg search "typing" --format json | jq '.[0].id'
91
+ ```
92
+
93
+ ## Guide coverage
94
+
95
+ 39 guides across 3 layers:
96
+
97
+ | Layer | Categories | Count | Examples |
98
+ |-------|-----------|-------|---------|
99
+ | **1 — stdlib** | typing, async, stdlib, data-structures | 16 | `list` over `List`, `match`/`case`, `TaskGroup` |
100
+ | **2 — frameworks** | pydantic, fastapi, httpx, django, sqlalchemy, pytest | 18 | Pydantic V2 migration, SQLAlchemy 2.0 style, `Annotated[Depends]` |
101
+ | **3 — toolchain** | toolchain | 5 | `uv` over `pip`, `ruff` over flake8, `pickle` avoidance |
102
+
103
+ Run `mpg list` to see all 39 guides, or [browse them on GitHub](skills/modern-python-guidance/guides/).
104
+
105
+ ## Version-aware filtering
106
+
107
+ Guides specify their minimum Python version. The CLI auto-detects your project's version from (in order):
108
+
109
+ 1. `--python-version` flag
110
+ 2. `pyproject.toml` `requires-python`
111
+ 3. `.python-version` file
112
+ 4. Default: 3.11
113
+
114
+ ```bash
115
+ # Only shows guides compatible with Python 3.9
116
+ mpg list --python-version 3.9
117
+ # Excludes: TaskGroup (3.11+), match/case (3.10+), etc.
118
+ ```
119
+
120
+ ## Development
121
+
122
+ ```bash
123
+ git clone https://github.com/yottayoshida/modern-python-guidance.git
124
+ cd modern-python-guidance
125
+ uv venv && source .venv/bin/activate
126
+ uv pip install -e ".[dev]"
127
+ pytest
128
+ ```
129
+
130
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for project structure and guide authoring details.
131
+
132
+ ## License
133
+
134
+ Apache-2.0 OR MIT — see [LICENSE](LICENSE) and [LICENSE-MIT](LICENSE-MIT).
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "benchmark-app"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = [
6
+ "fastapi",
7
+ "sqlalchemy[asyncio]",
8
+ "httpx",
9
+ "uvicorn",
10
+ ]
11
+
12
+ [tool.ruff]
13
+ target-version = "py312"
14
+
15
+ [tool.ruff.lint]
16
+ select = ["E", "F", "I", "UP"]
@@ -0,0 +1,36 @@
1
+ from collections.abc import AsyncGenerator
2
+ from contextlib import asynccontextmanager
3
+ from typing import Annotated
4
+
5
+ from fastapi import Depends, FastAPI
6
+ from sqlalchemy import select
7
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
8
+
9
+
10
+ engine = create_async_engine("sqlite+aiosqlite:///db.sqlite3")
11
+ async_session = async_sessionmaker(engine, expire_on_commit=False)
12
+
13
+
14
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
15
+ async with async_session() as session:
16
+ yield session
17
+
18
+
19
+ DbSession = Annotated[AsyncSession, Depends(get_db)]
20
+
21
+
22
+ @asynccontextmanager
23
+ async def lifespan(app: FastAPI):
24
+ async with engine.begin() as conn:
25
+ pass
26
+ yield {"db_pool": engine}
27
+ await engine.dispose()
28
+
29
+
30
+ app = FastAPI(lifespan=lifespan)
31
+
32
+
33
+ @app.get("/users")
34
+ async def list_users(db: DbSession):
35
+ result = await db.execute(select(User))
36
+ return result.scalars().all()
@@ -0,0 +1,20 @@
1
+ import tomllib
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from datetime import datetime, UTC
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class AppConfig:
9
+ db_url: str
10
+ debug: bool = False
11
+ created_at: datetime = None
12
+
13
+ def __post_init__(self):
14
+ if self.created_at is None:
15
+ self.created_at = datetime.now(UTC)
16
+
17
+
18
+ def load_config(path: Path) -> dict:
19
+ with open(path, "rb") as f:
20
+ return tomllib.load(f)
@@ -0,0 +1,24 @@
1
+ import asyncio
2
+
3
+ import httpx
4
+
5
+
6
+ async def crawl(urls: list[str]) -> list[str]:
7
+ results = []
8
+ async with httpx.AsyncClient() as client:
9
+ async with asyncio.TaskGroup() as tg:
10
+ for url in urls:
11
+ tg.create_task(_fetch(client, url, results))
12
+ return results
13
+
14
+
15
+ async def _fetch(client: httpx.AsyncClient, url: str, results: list[str]) -> None:
16
+ async with asyncio.timeout(30):
17
+ response = await client.get(url)
18
+ results.append(response.text)
19
+
20
+
21
+ async def download_large(client: httpx.AsyncClient, url: str, dest: str) -> None:
22
+ async with client.stream("GET", url) as resp:
23
+ async for chunk in resp.aiter_bytes():
24
+ pass
@@ -0,0 +1,71 @@
1
+ from datetime import datetime, UTC
2
+ from typing import override
3
+
4
+ from pydantic import BaseModel, ConfigDict, field_validator, field_serializer, model_validator
5
+ from sqlalchemy import String, select
6
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
7
+
8
+
9
+ class Base(DeclarativeBase):
10
+ pass
11
+
12
+
13
+ class User(Base):
14
+ __tablename__ = "users"
15
+ id: Mapped[int] = mapped_column(primary_key=True)
16
+ name: Mapped[str] = mapped_column(String(100))
17
+ email: Mapped[str | None] = mapped_column(String(255), nullable=True)
18
+ created_at: Mapped[datetime] = mapped_column(default=datetime.now(UTC))
19
+
20
+
21
+ class UserCreate(BaseModel):
22
+ model_config = ConfigDict(str_strip_whitespace=True)
23
+
24
+ name: str
25
+ email: str | None = None
26
+
27
+ @field_validator("name")
28
+ @classmethod
29
+ def name_not_empty(cls, v: str) -> str:
30
+ if not v:
31
+ raise ValueError("Name cannot be empty")
32
+ return v
33
+
34
+ @field_serializer("email")
35
+ def mask_email(self, v: str | None) -> str | None:
36
+ if v is None:
37
+ return None
38
+ local, domain = v.split("@")
39
+ return f"{local[0]}***@{domain}"
40
+
41
+
42
+ class UserResponse(BaseModel):
43
+ id: int
44
+ name: str
45
+ email: str | None = None
46
+
47
+ @model_validator(mode="after")
48
+ def check_consistency(self):
49
+ return self
50
+
51
+ def to_dict(self) -> dict:
52
+ return self.model_dump()
53
+
54
+ @classmethod
55
+ def from_orm(cls, obj):
56
+ return cls.model_validate(obj, from_attributes=True)
57
+
58
+
59
+ class Registry[T]:
60
+ def __init__(self) -> None:
61
+ self._items: dict[str, T] = {}
62
+
63
+ @override
64
+ def __repr__(self) -> str:
65
+ return f"Registry({list(self._items.keys())})"
66
+
67
+ def add(self, key: str, value: T) -> None:
68
+ self._items[key] = value
69
+
70
+ def get(self, key: str) -> T | None:
71
+ return self._items.get(key)
@@ -0,0 +1,42 @@
1
+ import asyncio
2
+ from enum import Enum
3
+ from pathlib import Path
4
+
5
+
6
+ class FileCategory(Enum):
7
+ IMAGE = "image"
8
+ VIDEO = "video"
9
+ DOCUMENT = "document"
10
+ OTHER = "other"
11
+
12
+
13
+ def categorize(ext: str) -> FileCategory:
14
+ match ext:
15
+ case ".jpg" | ".png" | ".gif":
16
+ return FileCategory.IMAGE
17
+ case ".mp4" | ".avi":
18
+ return FileCategory.VIDEO
19
+ case ".pdf" | ".docx":
20
+ return FileCategory.DOCUMENT
21
+ case _:
22
+ return FileCategory.OTHER
23
+
24
+
25
+ def parse_log_line(line: str) -> str:
26
+ return line.removeprefix("[INFO] ").removesuffix("\n")
27
+
28
+
29
+ async def safe_scan(paths: list[Path]) -> list[FileCategory]:
30
+ results = []
31
+ try:
32
+ async with asyncio.TaskGroup() as tg:
33
+ for p in paths:
34
+ tg.create_task(_scan_one(p, results))
35
+ except* OSError as eg:
36
+ for exc in eg.exceptions:
37
+ print(f"OS error: {exc}")
38
+ return results
39
+
40
+
41
+ async def _scan_one(p: Path, results: list[FileCategory]) -> None:
42
+ results.append(categorize(p.suffix))
@@ -0,0 +1,17 @@
1
+ import subprocess
2
+ from typing import ParamSpec, TypeIs
3
+
4
+ P = ParamSpec("P")
5
+
6
+
7
+ def merge_defaults(user: dict, defaults: dict) -> dict:
8
+ return defaults | user
9
+
10
+
11
+ def run_command(cmd: str, *extra: str) -> str:
12
+ result = subprocess.run([cmd, *extra], capture_output=True, text=True, check=True)
13
+ return result.stdout
14
+
15
+
16
+ def is_positive_int(val: object) -> TypeIs[int]:
17
+ return isinstance(val, int) and val > 0
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "benchmark-app"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = [
6
+ "fastapi",
7
+ "sqlalchemy",
8
+ "httpx",
9
+ "uvicorn",
10
+ ]
11
+
12
+ [tool.black]
13
+ line-length = 88
14
+
15
+ [tool.isort]
16
+ profile = "black"
17
+
18
+ [tool.flake8]
19
+ max-line-length = 88
@@ -0,0 +1,7 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="benchmark-app",
5
+ version="0.1.0",
6
+ packages=find_packages(),
7
+ )
@@ -0,0 +1,32 @@
1
+ from fastapi import Depends, FastAPI
2
+ from sqlalchemy import create_engine
3
+ from sqlalchemy.orm import Session, sessionmaker
4
+
5
+ engine = create_engine("sqlite:///db.sqlite3")
6
+ SessionLocal = sessionmaker(bind=engine)
7
+
8
+ app = FastAPI()
9
+
10
+
11
+ @app.on_event("startup")
12
+ async def startup():
13
+ app.state.db_engine = engine
14
+
15
+
16
+ @app.on_event("shutdown")
17
+ async def shutdown():
18
+ engine.dispose()
19
+
20
+
21
+ def get_db():
22
+ db = SessionLocal()
23
+ try:
24
+ yield db
25
+ finally:
26
+ db.close()
27
+
28
+
29
+ @app.get("/users")
30
+ def list_users(db: Session = Depends(get_db)):
31
+ users = db.query(User).all()
32
+ return users