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.
Files changed (45) hide show
  1. modern_python_guidance/__init__.py +3 -0
  2. modern_python_guidance/__main__.py +5 -0
  3. modern_python_guidance/cli.py +202 -0
  4. modern_python_guidance/compat.py +22 -0
  5. modern_python_guidance/frontmatter.py +166 -0
  6. modern_python_guidance/guide_index.py +96 -0
  7. modern_python_guidance/retrieve.py +56 -0
  8. modern_python_guidance/search.py +149 -0
  9. modern_python_guidance/skills/modern-python-guidance/SKILL.md +104 -0
  10. modern_python_guidance/skills/modern-python-guidance/guides/async/async-timeout-context.md +65 -0
  11. modern_python_guidance/skills/modern-python-guidance/guides/async/exception-groups.md +70 -0
  12. modern_python_guidance/skills/modern-python-guidance/guides/async/taskgroup-over-gather.md +63 -0
  13. modern_python_guidance/skills/modern-python-guidance/guides/data-structures/dataclass-modern.md +73 -0
  14. modern_python_guidance/skills/modern-python-guidance/guides/data-structures/dict-merge-operator.md +63 -0
  15. modern_python_guidance/skills/modern-python-guidance/guides/data-structures/match-case-patterns.md +70 -0
  16. modern_python_guidance/skills/modern-python-guidance/guides/fastapi/fastapi-annotated-depends.md +80 -0
  17. modern_python_guidance/skills/modern-python-guidance/guides/fastapi/fastapi-lifespan.md +77 -0
  18. modern_python_guidance/skills/modern-python-guidance/guides/fastapi/fastapi-typed-state.md +76 -0
  19. modern_python_guidance/skills/modern-python-guidance/guides/httpx/httpx-async-client-reuse.md +70 -0
  20. modern_python_guidance/skills/modern-python-guidance/guides/httpx/httpx-streaming.md +66 -0
  21. modern_python_guidance/skills/modern-python-guidance/guides/pydantic/pydantic-v2-config.md +73 -0
  22. modern_python_guidance/skills/modern-python-guidance/guides/pydantic/pydantic-v2-model-api.md +79 -0
  23. modern_python_guidance/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md +71 -0
  24. modern_python_guidance/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md +83 -0
  25. modern_python_guidance/skills/modern-python-guidance/guides/stdlib/datetime-utc.md +56 -0
  26. modern_python_guidance/skills/modern-python-guidance/guides/stdlib/pathlib-over-os-path.md +68 -0
  27. modern_python_guidance/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md +64 -0
  28. modern_python_guidance/skills/modern-python-guidance/guides/stdlib/tomllib-builtin.md +59 -0
  29. modern_python_guidance/skills/modern-python-guidance/guides/toolchain/no-pickle.md +79 -0
  30. modern_python_guidance/skills/modern-python-guidance/guides/toolchain/pyproject-toml-over-setup.md +69 -0
  31. modern_python_guidance/skills/modern-python-guidance/guides/toolchain/ruff-over-flake8.md +90 -0
  32. modern_python_guidance/skills/modern-python-guidance/guides/toolchain/safe-subprocess.md +79 -0
  33. modern_python_guidance/skills/modern-python-guidance/guides/toolchain/uv-over-pip.md +68 -0
  34. modern_python_guidance/skills/modern-python-guidance/guides/typing/override-decorator.md +65 -0
  35. modern_python_guidance/skills/modern-python-guidance/guides/typing/paramspec-decorators.md +81 -0
  36. modern_python_guidance/skills/modern-python-guidance/guides/typing/type-parameter-syntax.md +66 -0
  37. modern_python_guidance/skills/modern-python-guidance/guides/typing/typeis-vs-typeguard.md +66 -0
  38. modern_python_guidance/skills/modern-python-guidance/guides/typing/union-syntax.md +59 -0
  39. modern_python_guidance/skills/modern-python-guidance/guides/typing/use-builtin-generics.md +61 -0
  40. modern_python_guidance/version_detect.py +136 -0
  41. modern_python_guidance-0.1.0.dist-info/METADATA +180 -0
  42. modern_python_guidance-0.1.0.dist-info/RECORD +45 -0
  43. modern_python_guidance-0.1.0.dist-info/WHEEL +4 -0
  44. modern_python_guidance-0.1.0.dist-info/entry_points.txt +3 -0
  45. 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)
@@ -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)
@@ -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/)
@@ -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/)
@@ -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/)