modern-python-guidance 0.1.0__py3-none-any.whl
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.
- modern_python_guidance/__init__.py +3 -0
- modern_python_guidance/__main__.py +5 -0
- modern_python_guidance/cli.py +202 -0
- modern_python_guidance/compat.py +22 -0
- modern_python_guidance/frontmatter.py +166 -0
- modern_python_guidance/guide_index.py +96 -0
- modern_python_guidance/retrieve.py +56 -0
- modern_python_guidance/search.py +149 -0
- modern_python_guidance/skills/modern-python-guidance/SKILL.md +104 -0
- modern_python_guidance/skills/modern-python-guidance/guides/async/async-timeout-context.md +65 -0
- modern_python_guidance/skills/modern-python-guidance/guides/async/exception-groups.md +70 -0
- modern_python_guidance/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +63 -0
- modern_python_guidance/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +73 -0
- modern_python_guidance/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +63 -0
- modern_python_guidance/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +70 -0
- modern_python_guidance/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +80 -0
- modern_python_guidance/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +77 -0
- modern_python_guidance/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +76 -0
- modern_python_guidance/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +70 -0
- modern_python_guidance/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +66 -0
- modern_python_guidance/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +73 -0
- modern_python_guidance/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +79 -0
- modern_python_guidance/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +71 -0
- modern_python_guidance/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +83 -0
- modern_python_guidance/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +56 -0
- modern_python_guidance/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +68 -0
- modern_python_guidance/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +64 -0
- modern_python_guidance/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +59 -0
- modern_python_guidance/skills/modern-python-guidance/guides/toolchain/no-pickle.md +79 -0
- modern_python_guidance/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +69 -0
- modern_python_guidance/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +90 -0
- modern_python_guidance/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +79 -0
- modern_python_guidance/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +68 -0
- modern_python_guidance/skills/modern-python-guidance/guides/typing/override-decorator.md +65 -0
- modern_python_guidance/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +81 -0
- modern_python_guidance/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +66 -0
- modern_python_guidance/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +66 -0
- modern_python_guidance/skills/modern-python-guidance/guides/typing/union-syntax.md +59 -0
- modern_python_guidance/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +61 -0
- modern_python_guidance/version_detect.py +136 -0
- modern_python_guidance-0.1.0.dist-info/METADATA +180 -0
- modern_python_guidance-0.1.0.dist-info/RECORD +45 -0
- modern_python_guidance-0.1.0.dist-info/WHEEL +4 -0
- modern_python_guidance-0.1.0.dist-info/entry_points.txt +3 -0
- modern_python_guidance-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: modern-python-guidance
|
|
3
|
+
description: Version-aware BAD/GOOD pattern guides for modern Python. Use when writing, reviewing, or refactoring Python code to avoid outdated patterns (e.g. typing.List → list, @validator → @field_validator, setup.py → pyproject.toml). Triggers on "Python", "modernize", "upgrade", "deprecated", "pydantic", "fastapi", "httpx", "typing", "dataclass", "asyncio".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Modern Python Guidance
|
|
7
|
+
|
|
8
|
+
Version-aware BAD → GOOD pattern guides for modern Python (3.9–3.13+).
|
|
9
|
+
|
|
10
|
+
When writing or reviewing Python code, consult these guides to ensure modern idioms are used instead of deprecated or outdated patterns. Each guide shows a concrete BAD example, the modern GOOD replacement, and explains why the change matters.
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
- Writing new Python code (use modern patterns from the start)
|
|
15
|
+
- Reviewing Python code (flag outdated patterns)
|
|
16
|
+
- Migrating from Pydantic V1 to V2
|
|
17
|
+
- Upgrading Python version (check which new features are available)
|
|
18
|
+
- Replacing legacy tooling (setup.py, flake8, pip)
|
|
19
|
+
|
|
20
|
+
## Guide inventory (30 guides)
|
|
21
|
+
|
|
22
|
+
### Layer 1 — Standard Library & Language Features
|
|
23
|
+
|
|
24
|
+
| Category | Guide | Python | What it replaces |
|
|
25
|
+
|----------|-------|--------|-----------------|
|
|
26
|
+
| typing | `use-builtin-generics` | >=3.9 | `typing.List` → `list` |
|
|
27
|
+
| typing | `union-syntax` | >=3.10 | `Optional[X]` → `X \| None` |
|
|
28
|
+
| typing | `type-parameter-syntax` | >=3.12 | `TypeVar("T")` → `[T]` |
|
|
29
|
+
| typing | `override-decorator` | >=3.12 | manual override → `@override` |
|
|
30
|
+
| typing | `typeis-vs-typeguard` | >=3.13 | `TypeGuard` → `TypeIs` |
|
|
31
|
+
| typing | `paramspec-decorators` | >=3.10 | untyped decorators → `ParamSpec` |
|
|
32
|
+
| async | `taskgroup-over-gather` | >=3.11 | `gather()` → `TaskGroup` |
|
|
33
|
+
| async | `exception-groups` | >=3.11 | multi-error handling → `except*` |
|
|
34
|
+
| async | `async-timeout-context` | >=3.11 | `wait_for()` → `asyncio.timeout` |
|
|
35
|
+
| stdlib | `datetime-utc` | >=3.11 | `utcnow()` → `now(UTC)` |
|
|
36
|
+
| stdlib | `pathlib-over-os-path` | >=3.9 | `os.path` → `pathlib.Path` |
|
|
37
|
+
| stdlib | `tomllib-builtin` | >=3.11 | `toml` package → `tomllib` |
|
|
38
|
+
| stdlib | `removeprefix-removesuffix` | >=3.9 | `lstrip()`/slicing → `removeprefix()` |
|
|
39
|
+
| data-structures | `dict-merge-operator` | >=3.9 | `{**d1, **d2}` → `d1 \| d2` |
|
|
40
|
+
| data-structures | `match-case-patterns` | >=3.10 | nested if/isinstance → `match`/`case` |
|
|
41
|
+
| data-structures | `dataclass-modern` | >=3.10 | basic dataclass → `slots=True, kw_only=True` |
|
|
42
|
+
|
|
43
|
+
### Layer 2 — Popular Frameworks
|
|
44
|
+
|
|
45
|
+
| Category | Guide | What it replaces |
|
|
46
|
+
|----------|-------|-----------------|
|
|
47
|
+
| pydantic | `pydantic-v2-model-api` | `parse_obj()` → `model_validate()` |
|
|
48
|
+
| pydantic | `pydantic-v2-validators` | `@validator` → `@field_validator` |
|
|
49
|
+
| pydantic | `pydantic-v2-config` | `class Config` → `model_config = ConfigDict(...)` |
|
|
50
|
+
| pydantic | `pydantic-v2-serialization` | `json_encoders` → `@field_serializer` |
|
|
51
|
+
| fastapi | `fastapi-lifespan` | `@on_event` → lifespan context manager |
|
|
52
|
+
| fastapi | `fastapi-annotated-depends` | `Depends()` default → `Annotated[T, Depends()]` |
|
|
53
|
+
| fastapi | `fastapi-typed-state` | untyped `app.state` → typed state via lifespan |
|
|
54
|
+
| httpx | `httpx-async-client-reuse` | per-request client → shared `AsyncClient` |
|
|
55
|
+
| httpx | `httpx-streaming` | `response.content` → `client.stream()` |
|
|
56
|
+
|
|
57
|
+
### Layer 3 — Toolchain & Security
|
|
58
|
+
|
|
59
|
+
| Category | Guide | What it replaces |
|
|
60
|
+
|----------|-------|-----------------|
|
|
61
|
+
| toolchain | `pyproject-toml-over-setup` | `setup.py` → `pyproject.toml` |
|
|
62
|
+
| toolchain | `uv-over-pip` | `pip` → `uv` |
|
|
63
|
+
| toolchain | `ruff-over-flake8` | flake8+isort+black → `ruff` |
|
|
64
|
+
| toolchain | `no-pickle` | `pickle.load()` → safe alternatives |
|
|
65
|
+
| toolchain | `safe-subprocess` | `shell=True` → list arguments |
|
|
66
|
+
|
|
67
|
+
## How to look up a guide
|
|
68
|
+
|
|
69
|
+
Each guide is a markdown file in `guides/<category>/<id>.md` with YAML frontmatter containing:
|
|
70
|
+
- `id`: unique identifier
|
|
71
|
+
- `python`: minimum Python version (e.g. `">=3.11"`)
|
|
72
|
+
- `frequency`: how often LLMs generate the outdated pattern (`high`/`medium`/`low`)
|
|
73
|
+
- `layer`: 1 (stdlib), 2 (frameworks), 3 (toolchain)
|
|
74
|
+
|
|
75
|
+
### Reading a guide directly
|
|
76
|
+
|
|
77
|
+
Open `guides/<category>/<guide-id>.md` for the full BAD/GOOD comparison.
|
|
78
|
+
|
|
79
|
+
### Using the CLI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Search by keyword
|
|
83
|
+
mpg search "typing list"
|
|
84
|
+
|
|
85
|
+
# Retrieve full guide content
|
|
86
|
+
mpg retrieve use-builtin-generics
|
|
87
|
+
|
|
88
|
+
# List all guides for a Python version
|
|
89
|
+
mpg list --python-version 3.11
|
|
90
|
+
|
|
91
|
+
# Detect project Python version
|
|
92
|
+
mpg detect-version
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Integration pattern for agents
|
|
96
|
+
|
|
97
|
+
When generating Python code:
|
|
98
|
+
|
|
99
|
+
1. Check if the target Python version is known (from `pyproject.toml`, `.python-version`, or context)
|
|
100
|
+
2. For each pattern you're about to write, check if a guide exists for a modern replacement
|
|
101
|
+
3. Use the GOOD pattern instead of the BAD pattern
|
|
102
|
+
4. If the target Python version is too old for the modern pattern, use the older pattern and note it
|
|
103
|
+
|
|
104
|
+
Example: if writing `from typing import List` for a Python 3.9+ project, use `list` instead (see `use-builtin-generics`).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: async-timeout-context
|
|
3
|
+
title: Use asyncio.timeout Instead of wait_for
|
|
4
|
+
category: async
|
|
5
|
+
layer: 1
|
|
6
|
+
tags:
|
|
7
|
+
- asyncio
|
|
8
|
+
- timeout
|
|
9
|
+
aliases:
|
|
10
|
+
- asyncio.wait_for
|
|
11
|
+
- asyncio.timeout
|
|
12
|
+
python: ">=3.11"
|
|
13
|
+
frequency: medium
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Use asyncio.timeout Instead of wait_for
|
|
17
|
+
|
|
18
|
+
Since Python 3.11, use the `asyncio.timeout` context manager instead of `asyncio.wait_for`. It provides structured cancellation and works with any block of async code.
|
|
19
|
+
|
|
20
|
+
## BAD
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import asyncio
|
|
24
|
+
|
|
25
|
+
async def fetch_data():
|
|
26
|
+
try:
|
|
27
|
+
result = await asyncio.wait_for(slow_operation(), timeout=5.0)
|
|
28
|
+
except asyncio.TimeoutError:
|
|
29
|
+
result = default_value
|
|
30
|
+
|
|
31
|
+
# wait_for only wraps a single awaitable
|
|
32
|
+
# Cannot timeout multiple operations together
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## GOOD
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
|
|
40
|
+
async def fetch_data():
|
|
41
|
+
try:
|
|
42
|
+
async with asyncio.timeout(5.0):
|
|
43
|
+
data = await slow_operation()
|
|
44
|
+
parsed = await parse(data)
|
|
45
|
+
# Both operations share the 5s budget
|
|
46
|
+
except TimeoutError:
|
|
47
|
+
return default_value
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Why
|
|
51
|
+
|
|
52
|
+
- Context manager scopes timeout to an entire block, not just one awaitable
|
|
53
|
+
- Multiple operations share the same deadline
|
|
54
|
+
- Raises `TimeoutError` (3.12+) or `asyncio.TimeoutError` (3.11) — catch `TimeoutError` to cover both
|
|
55
|
+
- `asyncio.timeout(None)` disables timeout (useful for conditional timeouts)
|
|
56
|
+
|
|
57
|
+
## Version Notes
|
|
58
|
+
|
|
59
|
+
- 3.11+: `asyncio.timeout(delay)` and `asyncio.timeout_at(when)`
|
|
60
|
+
- 3.12+: `asyncio.timeout` raises `TimeoutError` (not `asyncio.TimeoutError`)
|
|
61
|
+
- Pre-3.11: Use `async-timeout` package
|
|
62
|
+
|
|
63
|
+
## References
|
|
64
|
+
|
|
65
|
+
- [asyncio.timeout documentation](https://docs.python.org/3/library/asyncio-task.html#asyncio.timeout)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: exception-groups
|
|
3
|
+
title: Use except* for Exception Groups
|
|
4
|
+
category: async
|
|
5
|
+
layer: 1
|
|
6
|
+
tags:
|
|
7
|
+
- asyncio
|
|
8
|
+
- exceptions
|
|
9
|
+
- error-handling
|
|
10
|
+
aliases:
|
|
11
|
+
- ExceptionGroup
|
|
12
|
+
- except*
|
|
13
|
+
- BaseExceptionGroup
|
|
14
|
+
python: ">=3.11"
|
|
15
|
+
frequency: medium
|
|
16
|
+
pep: 654
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Use except* for Exception Groups
|
|
20
|
+
|
|
21
|
+
Since Python 3.11, use `except*` to handle multiple concurrent exceptions from `TaskGroup` and other async contexts.
|
|
22
|
+
|
|
23
|
+
## BAD
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import asyncio
|
|
27
|
+
|
|
28
|
+
async def main():
|
|
29
|
+
try:
|
|
30
|
+
async with asyncio.TaskGroup() as tg:
|
|
31
|
+
tg.create_task(fetch_users())
|
|
32
|
+
tg.create_task(fetch_orders())
|
|
33
|
+
except Exception as e:
|
|
34
|
+
# Only sees the ExceptionGroup wrapper, not individual errors
|
|
35
|
+
print(f"Something failed: {e}")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## GOOD
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
|
|
43
|
+
async def main():
|
|
44
|
+
try:
|
|
45
|
+
async with asyncio.TaskGroup() as tg:
|
|
46
|
+
tg.create_task(fetch_users())
|
|
47
|
+
tg.create_task(fetch_orders())
|
|
48
|
+
except* ValueError as eg:
|
|
49
|
+
for e in eg.exceptions:
|
|
50
|
+
print(f"Validation error: {e}")
|
|
51
|
+
except* ConnectionError as eg:
|
|
52
|
+
for e in eg.exceptions:
|
|
53
|
+
print(f"Connection failed: {e}")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Why
|
|
57
|
+
|
|
58
|
+
- `TaskGroup` raises `ExceptionGroup` when multiple tasks fail simultaneously
|
|
59
|
+
- `except*` matches and extracts specific exception types from the group
|
|
60
|
+
- Multiple `except*` clauses can each handle different types from the same group
|
|
61
|
+
- Traditional `except` sees only the `ExceptionGroup` wrapper
|
|
62
|
+
|
|
63
|
+
## Version Notes
|
|
64
|
+
|
|
65
|
+
- 3.11+: `except*`, `ExceptionGroup`, `BaseExceptionGroup`
|
|
66
|
+
- Pre-3.11: Use `exceptiongroup` backport package
|
|
67
|
+
|
|
68
|
+
## References
|
|
69
|
+
|
|
70
|
+
- [PEP 654 — Exception Groups and except*](https://peps.python.org/pep-0654/)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: taskgroup-over-gather
|
|
3
|
+
title: Use asyncio.TaskGroup Instead of asyncio.gather
|
|
4
|
+
category: async
|
|
5
|
+
layer: 1
|
|
6
|
+
tags:
|
|
7
|
+
- asyncio
|
|
8
|
+
- concurrency
|
|
9
|
+
- taskgroup
|
|
10
|
+
aliases:
|
|
11
|
+
- asyncio.gather
|
|
12
|
+
- gather
|
|
13
|
+
python: ">=3.11"
|
|
14
|
+
frequency: high
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Use asyncio.TaskGroup Instead of asyncio.gather
|
|
18
|
+
|
|
19
|
+
`asyncio.TaskGroup` provides structured concurrency with proper error handling. `asyncio.gather` silently drops errors from other tasks when one fails.
|
|
20
|
+
|
|
21
|
+
## BAD
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import asyncio
|
|
25
|
+
|
|
26
|
+
async def main():
|
|
27
|
+
results = await asyncio.gather(
|
|
28
|
+
fetch_users(),
|
|
29
|
+
fetch_orders(),
|
|
30
|
+
fetch_products(),
|
|
31
|
+
)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## GOOD
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import asyncio
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
async with asyncio.TaskGroup() as tg:
|
|
41
|
+
users_task = tg.create_task(fetch_users())
|
|
42
|
+
orders_task = tg.create_task(fetch_orders())
|
|
43
|
+
products_task = tg.create_task(fetch_products())
|
|
44
|
+
|
|
45
|
+
results = (users_task.result(), orders_task.result(), products_task.result())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Why
|
|
49
|
+
|
|
50
|
+
- `gather` with `return_exceptions=False` cancels remaining tasks on first error but may swallow secondary exceptions
|
|
51
|
+
- `TaskGroup` raises `ExceptionGroup` containing all failures
|
|
52
|
+
- Structured concurrency: all tasks are guaranteed to finish before the block exits
|
|
53
|
+
- Clearer intent — each task is named and individually accessible
|
|
54
|
+
|
|
55
|
+
## Version Notes
|
|
56
|
+
|
|
57
|
+
- 3.11+: `asyncio.TaskGroup` added
|
|
58
|
+
- For 3.10 and below, consider `anyio.create_task_group()` as a backport
|
|
59
|
+
|
|
60
|
+
## References
|
|
61
|
+
|
|
62
|
+
- [PEP 654 — Exception Groups](https://peps.python.org/pep-0654/)
|
|
63
|
+
- [asyncio.TaskGroup docs](https://docs.python.org/3/library/asyncio-task.html#asyncio.TaskGroup)
|
modern_python_guidance/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: dataclass-modern
|
|
3
|
+
title: Use Modern Dataclass Features (slots, kw_only)
|
|
4
|
+
category: data-structures
|
|
5
|
+
layer: 1
|
|
6
|
+
tags:
|
|
7
|
+
- dataclass
|
|
8
|
+
- slots
|
|
9
|
+
- kw_only
|
|
10
|
+
aliases:
|
|
11
|
+
- dataclass
|
|
12
|
+
- dataclasses
|
|
13
|
+
python: ">=3.10"
|
|
14
|
+
frequency: medium
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Use Modern Dataclass Features
|
|
18
|
+
|
|
19
|
+
Since Python 3.10, dataclasses support `slots=True` and `kw_only=True` for better performance and safer APIs.
|
|
20
|
+
|
|
21
|
+
## BAD
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Point:
|
|
28
|
+
x: float
|
|
29
|
+
y: float
|
|
30
|
+
z: float = 0.0
|
|
31
|
+
|
|
32
|
+
# No slots: allows typos on attributes
|
|
33
|
+
p = Point(1.0, 2.0)
|
|
34
|
+
p.w = 3.0 # silently creates new attribute (typo for 'z')
|
|
35
|
+
|
|
36
|
+
# Positional args: easy to mix up x and y
|
|
37
|
+
p = Point(2.0, 1.0) # is this (x=2, y=1) or (x=1, y=2)?
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## GOOD
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True, kw_only=True)
|
|
46
|
+
class Point:
|
|
47
|
+
x: float
|
|
48
|
+
y: float
|
|
49
|
+
z: float = 0.0
|
|
50
|
+
|
|
51
|
+
p = Point(x=1.0, y=2.0)
|
|
52
|
+
p.w = 3.0 # AttributeError: 'Point' has no attribute 'w'
|
|
53
|
+
|
|
54
|
+
# kw_only forces explicit names — no positional confusion
|
|
55
|
+
p = Point(x=2.0, y=1.0) # intent is clear
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Why
|
|
59
|
+
|
|
60
|
+
- `slots=True`: 20-35% less memory, faster attribute access, prevents typo attributes
|
|
61
|
+
- `kw_only=True`: forces named arguments, eliminates positional ordering bugs
|
|
62
|
+
- `frozen=True` + `slots=True`: fast immutable value objects
|
|
63
|
+
- Combine for production data classes: `@dataclass(slots=True, frozen=True, kw_only=True)`
|
|
64
|
+
|
|
65
|
+
## Version Notes
|
|
66
|
+
|
|
67
|
+
- 3.10+: `slots=True`, `kw_only=True`
|
|
68
|
+
- 3.10+: Per-field `kw_only` via `field(kw_only=True)`
|
|
69
|
+
- 3.7-3.9: Basic `@dataclass` without slots/kw_only
|
|
70
|
+
|
|
71
|
+
## References
|
|
72
|
+
|
|
73
|
+
- [dataclasses documentation](https://docs.python.org/3/library/dataclasses.html)
|
modern_python_guidance/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: dict-merge-operator
|
|
3
|
+
title: Use | Operator for Dict Merging
|
|
4
|
+
category: data-structures
|
|
5
|
+
layer: 1
|
|
6
|
+
tags:
|
|
7
|
+
- dict
|
|
8
|
+
- merge
|
|
9
|
+
- operator
|
|
10
|
+
aliases:
|
|
11
|
+
- dict merge
|
|
12
|
+
- dict update
|
|
13
|
+
- "**kwargs"
|
|
14
|
+
python: ">=3.9"
|
|
15
|
+
frequency: medium
|
|
16
|
+
pep: 584
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Use | Operator for Dict Merging
|
|
20
|
+
|
|
21
|
+
Since Python 3.9, use the `|` operator to merge dictionaries instead of `{**d1, **d2}` or `dict.update()`.
|
|
22
|
+
|
|
23
|
+
## BAD
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
defaults = {"timeout": 30, "retries": 3}
|
|
27
|
+
overrides = {"timeout": 60, "verbose": True}
|
|
28
|
+
|
|
29
|
+
# Unpacking merge — unclear precedence
|
|
30
|
+
config = {**defaults, **overrides}
|
|
31
|
+
|
|
32
|
+
# Mutates defaults
|
|
33
|
+
defaults.update(overrides)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## GOOD
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
defaults = {"timeout": 30, "retries": 3}
|
|
40
|
+
overrides = {"timeout": 60, "verbose": True}
|
|
41
|
+
|
|
42
|
+
# Merge (right side wins on conflicts)
|
|
43
|
+
config = defaults | overrides
|
|
44
|
+
|
|
45
|
+
# In-place merge
|
|
46
|
+
defaults |= overrides
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Why
|
|
50
|
+
|
|
51
|
+
- Cleaner syntax: `d1 | d2` is immediately obvious
|
|
52
|
+
- Non-mutating by default (like set `|`)
|
|
53
|
+
- `|=` for in-place update (like `+=`)
|
|
54
|
+
- Works with dict subclasses (unlike `{**d1, **d2}`)
|
|
55
|
+
|
|
56
|
+
## Version Notes
|
|
57
|
+
|
|
58
|
+
- 3.9+: `dict.__or__` and `dict.__ior__`
|
|
59
|
+
- Pre-3.9: `{**d1, **d2}` or `d1.update(d2)` (mutating)
|
|
60
|
+
|
|
61
|
+
## References
|
|
62
|
+
|
|
63
|
+
- [PEP 584 — Add Union Operators To dict](https://peps.python.org/pep-0584/)
|
modern_python_guidance/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: match-case-patterns
|
|
3
|
+
title: Use Structural Pattern Matching
|
|
4
|
+
category: data-structures
|
|
5
|
+
layer: 1
|
|
6
|
+
tags:
|
|
7
|
+
- match
|
|
8
|
+
- pattern-matching
|
|
9
|
+
- control-flow
|
|
10
|
+
aliases:
|
|
11
|
+
- match case
|
|
12
|
+
- switch case
|
|
13
|
+
- pattern matching
|
|
14
|
+
python: ">=3.10"
|
|
15
|
+
frequency: medium
|
|
16
|
+
pep: 634
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Use Structural Pattern Matching
|
|
20
|
+
|
|
21
|
+
Since Python 3.10, use `match`/`case` for complex conditional logic involving structure, type, and value checks.
|
|
22
|
+
|
|
23
|
+
## BAD
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
def handle_command(command):
|
|
27
|
+
if isinstance(command, dict):
|
|
28
|
+
if "action" in command and command["action"] == "move":
|
|
29
|
+
x = command.get("x", 0)
|
|
30
|
+
y = command.get("y", 0)
|
|
31
|
+
return move(x, y)
|
|
32
|
+
elif "action" in command and command["action"] == "resize":
|
|
33
|
+
return resize(command.get("width"), command.get("height"))
|
|
34
|
+
elif isinstance(command, list) and len(command) == 2:
|
|
35
|
+
return move(*command)
|
|
36
|
+
raise ValueError(f"Unknown command: {command}")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## GOOD
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
def handle_command(command):
|
|
43
|
+
match command:
|
|
44
|
+
case {"action": "move", "x": x, "y": y}:
|
|
45
|
+
return move(x, y)
|
|
46
|
+
case {"action": "resize", "width": w, "height": h}:
|
|
47
|
+
return resize(w, h)
|
|
48
|
+
case [x, y]:
|
|
49
|
+
return move(x, y)
|
|
50
|
+
case _:
|
|
51
|
+
raise ValueError(f"Unknown command: {command}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Why
|
|
55
|
+
|
|
56
|
+
- Destructures and binds in one step
|
|
57
|
+
- Handles dicts, sequences, classes, and literals uniformly
|
|
58
|
+
- Guard clauses with `case ... if condition:`
|
|
59
|
+
- `_` wildcard is explicit "match anything"
|
|
60
|
+
- More readable than nested `if/isinstance/in` chains
|
|
61
|
+
|
|
62
|
+
## Version Notes
|
|
63
|
+
|
|
64
|
+
- 3.10+: `match`/`case` statements
|
|
65
|
+
- Not a switch statement — it's structural pattern matching with binding
|
|
66
|
+
|
|
67
|
+
## References
|
|
68
|
+
|
|
69
|
+
- [PEP 634 — Structural Pattern Matching: Specification](https://peps.python.org/pep-0634/)
|
|
70
|
+
- [PEP 636 — Structural Pattern Matching: Tutorial](https://peps.python.org/pep-0636/)
|
modern_python_guidance/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: fastapi-annotated-depends
|
|
3
|
+
title: Use Annotated for Dependency Injection
|
|
4
|
+
category: fastapi
|
|
5
|
+
layer: 2
|
|
6
|
+
tags:
|
|
7
|
+
- fastapi
|
|
8
|
+
- dependency-injection
|
|
9
|
+
- annotated
|
|
10
|
+
aliases:
|
|
11
|
+
- Depends
|
|
12
|
+
- Annotated
|
|
13
|
+
- dependency injection
|
|
14
|
+
python: ">=3.9"
|
|
15
|
+
frequency: high
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Use Annotated for Dependency Injection
|
|
19
|
+
|
|
20
|
+
Since FastAPI 0.95.0, use `Annotated[T, Depends(...)]` instead of bare `Depends()` as default values.
|
|
21
|
+
|
|
22
|
+
## BAD
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from fastapi import Depends, FastAPI
|
|
26
|
+
|
|
27
|
+
app = FastAPI()
|
|
28
|
+
|
|
29
|
+
async def get_db():
|
|
30
|
+
db = SessionLocal()
|
|
31
|
+
try:
|
|
32
|
+
yield db
|
|
33
|
+
finally:
|
|
34
|
+
db.close()
|
|
35
|
+
|
|
36
|
+
@app.get("/users")
|
|
37
|
+
async def list_users(db: Session = Depends(get_db)):
|
|
38
|
+
return db.query(User).all()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## GOOD
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from typing import Annotated
|
|
45
|
+
|
|
46
|
+
from fastapi import Depends, FastAPI
|
|
47
|
+
|
|
48
|
+
app = FastAPI()
|
|
49
|
+
|
|
50
|
+
async def get_db():
|
|
51
|
+
db = SessionLocal()
|
|
52
|
+
try:
|
|
53
|
+
yield db
|
|
54
|
+
finally:
|
|
55
|
+
db.close()
|
|
56
|
+
|
|
57
|
+
DbDep = Annotated[Session, Depends(get_db)]
|
|
58
|
+
|
|
59
|
+
@app.get("/users")
|
|
60
|
+
async def list_users(db: DbDep):
|
|
61
|
+
return db.query(User).all()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Why
|
|
65
|
+
|
|
66
|
+
- `Annotated` keeps the type and the dependency together — reusable as a type alias
|
|
67
|
+
- Default values with `Depends()` don't work well with non-FastAPI callers (e.g., tests)
|
|
68
|
+
- `Annotated` is the standard Python way to attach metadata to types (PEP 593)
|
|
69
|
+
- Multiple dependencies can be composed into a single type alias
|
|
70
|
+
|
|
71
|
+
## Version Notes
|
|
72
|
+
|
|
73
|
+
- `Annotated` available from `typing` since 3.9
|
|
74
|
+
- FastAPI `Annotated` support since 0.95.0 (2023-04)
|
|
75
|
+
- Also works for `Query`, `Path`, `Body`, `Header`, `Cookie`, `Form`, `File`
|
|
76
|
+
|
|
77
|
+
## References
|
|
78
|
+
|
|
79
|
+
- [FastAPI Dependencies with Annotated](https://fastapi.tiangolo.com/tutorial/dependencies/)
|
|
80
|
+
- [PEP 593 — Flexible function and variable annotations](https://peps.python.org/pep-0593/)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: fastapi-lifespan
|
|
3
|
+
title: Use Lifespan Context Manager Instead of on_event
|
|
4
|
+
category: fastapi
|
|
5
|
+
layer: 2
|
|
6
|
+
tags:
|
|
7
|
+
- fastapi
|
|
8
|
+
- lifespan
|
|
9
|
+
- startup
|
|
10
|
+
- shutdown
|
|
11
|
+
aliases:
|
|
12
|
+
- on_event
|
|
13
|
+
- startup
|
|
14
|
+
- shutdown
|
|
15
|
+
python: ">=3.9"
|
|
16
|
+
frequency: high
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Use Lifespan Context Manager
|
|
20
|
+
|
|
21
|
+
FastAPI's `@app.on_event("startup")` and `@app.on_event("shutdown")` decorators are deprecated. Use the lifespan context manager instead.
|
|
22
|
+
|
|
23
|
+
## BAD
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from fastapi import FastAPI
|
|
27
|
+
|
|
28
|
+
app = FastAPI()
|
|
29
|
+
db_pool = None
|
|
30
|
+
|
|
31
|
+
@app.on_event("startup")
|
|
32
|
+
async def startup():
|
|
33
|
+
global db_pool
|
|
34
|
+
db_pool = await create_pool()
|
|
35
|
+
|
|
36
|
+
@app.on_event("shutdown")
|
|
37
|
+
async def shutdown():
|
|
38
|
+
await db_pool.close()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## GOOD
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from collections.abc import AsyncIterator
|
|
45
|
+
from contextlib import asynccontextmanager
|
|
46
|
+
|
|
47
|
+
from fastapi import FastAPI
|
|
48
|
+
|
|
49
|
+
@asynccontextmanager
|
|
50
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[dict]:
|
|
51
|
+
pool = await create_pool()
|
|
52
|
+
yield {"db_pool": pool}
|
|
53
|
+
await pool.close()
|
|
54
|
+
|
|
55
|
+
app = FastAPI(lifespan=lifespan)
|
|
56
|
+
|
|
57
|
+
@app.get("/")
|
|
58
|
+
async def root(request: Request):
|
|
59
|
+
pool = request.state.db_pool
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Why
|
|
63
|
+
|
|
64
|
+
- `on_event` is deprecated since FastAPI 0.93.0 (2023-02)
|
|
65
|
+
- Lifespan provides typed state access via `request.state`
|
|
66
|
+
- Resource cleanup is guaranteed by the context manager protocol
|
|
67
|
+
- Easier to test — lifespan is a plain async function
|
|
68
|
+
|
|
69
|
+
## Version Notes
|
|
70
|
+
|
|
71
|
+
- Works on Python 3.9+ with FastAPI >= 0.93.0
|
|
72
|
+
- `AsyncIterator` moved from `typing` to `collections.abc` in 3.9
|
|
73
|
+
|
|
74
|
+
## References
|
|
75
|
+
|
|
76
|
+
- [FastAPI Lifespan Events](https://fastapi.tiangolo.com/advanced/events/)
|
|
77
|
+
- [Starlette Lifespan](https://www.starlette.io/lifespan/)
|