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,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: fastapi-typed-state
|
|
3
|
+
title: Use TypedDict or dataclass for App State
|
|
4
|
+
category: fastapi
|
|
5
|
+
layer: 2
|
|
6
|
+
tags:
|
|
7
|
+
- fastapi
|
|
8
|
+
- state
|
|
9
|
+
- typed
|
|
10
|
+
aliases:
|
|
11
|
+
- app.state
|
|
12
|
+
- request.state
|
|
13
|
+
- typed state
|
|
14
|
+
python: ">=3.9"
|
|
15
|
+
frequency: medium
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Use TypedDict or dataclass for App State
|
|
19
|
+
|
|
20
|
+
Instead of untyped `app.state.foo` attribute access, use a typed state dict via the lifespan pattern.
|
|
21
|
+
|
|
22
|
+
## BAD
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from fastapi import FastAPI, Request
|
|
26
|
+
|
|
27
|
+
app = FastAPI()
|
|
28
|
+
app.state.db_pool = create_pool()
|
|
29
|
+
app.state.cache = create_cache()
|
|
30
|
+
|
|
31
|
+
@app.get("/")
|
|
32
|
+
async def root(request: Request):
|
|
33
|
+
pool = request.state.db_pool # Any — no type checking
|
|
34
|
+
cache = request.state.cche # typo silently passes
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## GOOD
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from collections.abc import AsyncIterator
|
|
41
|
+
from contextlib import asynccontextmanager
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
|
|
44
|
+
from fastapi import FastAPI, Request
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class AppState:
|
|
48
|
+
db_pool: Pool
|
|
49
|
+
cache: Cache
|
|
50
|
+
|
|
51
|
+
@asynccontextmanager
|
|
52
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[dict]:
|
|
53
|
+
pool = await create_pool()
|
|
54
|
+
cache = await create_cache()
|
|
55
|
+
yield {"db_pool": pool, "cache": cache}
|
|
56
|
+
await cache.close()
|
|
57
|
+
await pool.close()
|
|
58
|
+
|
|
59
|
+
app = FastAPI(lifespan=lifespan)
|
|
60
|
+
|
|
61
|
+
@app.get("/")
|
|
62
|
+
async def root(request: Request):
|
|
63
|
+
pool = request.state.db_pool # accessible via lifespan state
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Why
|
|
67
|
+
|
|
68
|
+
- `app.state` is untyped — typos and missing attributes are invisible to type checkers
|
|
69
|
+
- Lifespan state dict makes initialization and cleanup explicit
|
|
70
|
+
- `dataclass` or `TypedDict` documents the expected shape
|
|
71
|
+
- Resource cleanup is guaranteed by the context manager
|
|
72
|
+
|
|
73
|
+
## References
|
|
74
|
+
|
|
75
|
+
- [FastAPI Lifespan State](https://fastapi.tiangolo.com/advanced/events/#lifespan-state)
|
|
76
|
+
- [Starlette State](https://www.starlette.io/lifespan/)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: httpx-async-client-reuse
|
|
3
|
+
title: Reuse httpx.AsyncClient Instead of Creating Per-Request
|
|
4
|
+
category: httpx
|
|
5
|
+
layer: 2
|
|
6
|
+
tags:
|
|
7
|
+
- httpx
|
|
8
|
+
- async
|
|
9
|
+
- connection-pooling
|
|
10
|
+
aliases:
|
|
11
|
+
- httpx
|
|
12
|
+
- AsyncClient
|
|
13
|
+
- aiohttp
|
|
14
|
+
- requests
|
|
15
|
+
python: ">=3.9"
|
|
16
|
+
frequency: high
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Reuse httpx.AsyncClient
|
|
20
|
+
|
|
21
|
+
Create one `httpx.AsyncClient` and reuse it across requests instead of creating a new client per call.
|
|
22
|
+
|
|
23
|
+
## BAD
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import httpx
|
|
27
|
+
|
|
28
|
+
async def fetch_user(user_id: int) -> dict:
|
|
29
|
+
async with httpx.AsyncClient() as client:
|
|
30
|
+
resp = await client.get(f"https://api.example.com/users/{user_id}")
|
|
31
|
+
return resp.json()
|
|
32
|
+
|
|
33
|
+
async def fetch_many(ids: list[int]) -> list[dict]:
|
|
34
|
+
return [await fetch_user(i) for i in ids] # new client per call
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## GOOD
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import httpx
|
|
41
|
+
|
|
42
|
+
async def fetch_many(ids: list[int]) -> list[dict]:
|
|
43
|
+
async with httpx.AsyncClient(base_url="https://api.example.com") as client:
|
|
44
|
+
results = []
|
|
45
|
+
for user_id in ids:
|
|
46
|
+
resp = await client.get(f"/users/{user_id}")
|
|
47
|
+
resp.raise_for_status()
|
|
48
|
+
results.append(resp.json())
|
|
49
|
+
return results
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Why
|
|
53
|
+
|
|
54
|
+
- Per-request clients skip connection pooling — each call opens a new TCP+TLS handshake
|
|
55
|
+
- `AsyncClient` maintains a connection pool, reusing connections across requests
|
|
56
|
+
- `httpx` is the modern replacement for `requests` (sync) and `aiohttp` (async)
|
|
57
|
+
- Context manager ensures connections are properly closed
|
|
58
|
+
- `base_url` eliminates URL duplication
|
|
59
|
+
|
|
60
|
+
## Version Notes
|
|
61
|
+
|
|
62
|
+
- `httpx` is a third-party package, not stdlib
|
|
63
|
+
- Works on Python 3.9+ with `httpx >= 0.23`
|
|
64
|
+
- Preferred over `requests` for new async code
|
|
65
|
+
- Preferred over `aiohttp` for simpler API and `requests`-like interface
|
|
66
|
+
|
|
67
|
+
## References
|
|
68
|
+
|
|
69
|
+
- [httpx Async Client](https://www.python-httpx.org/async/)
|
|
70
|
+
- [httpx Connection Pooling](https://www.python-httpx.org/advanced/clients/)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: httpx-streaming
|
|
3
|
+
title: Use httpx Streaming for Large Responses
|
|
4
|
+
category: httpx
|
|
5
|
+
layer: 2
|
|
6
|
+
tags:
|
|
7
|
+
- httpx
|
|
8
|
+
- streaming
|
|
9
|
+
- memory
|
|
10
|
+
aliases:
|
|
11
|
+
- streaming
|
|
12
|
+
- stream
|
|
13
|
+
- large response
|
|
14
|
+
python: ">=3.9"
|
|
15
|
+
frequency: medium
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Use httpx Streaming for Large Responses
|
|
19
|
+
|
|
20
|
+
Use `client.stream()` instead of `client.get()` for large responses to avoid loading the entire body into memory.
|
|
21
|
+
|
|
22
|
+
## BAD
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import httpx
|
|
26
|
+
|
|
27
|
+
async def download_file(url: str, path: str) -> None:
|
|
28
|
+
async with httpx.AsyncClient() as client:
|
|
29
|
+
response = await client.get(url)
|
|
30
|
+
with open(path, "wb") as f:
|
|
31
|
+
f.write(response.content) # entire file in memory
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## GOOD
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import httpx
|
|
38
|
+
|
|
39
|
+
async def download_file(url: str, path: str) -> None:
|
|
40
|
+
async with httpx.AsyncClient() as client:
|
|
41
|
+
async with client.stream("GET", url) as response:
|
|
42
|
+
response.raise_for_status()
|
|
43
|
+
with open(path, "wb") as f:
|
|
44
|
+
async for chunk in response.aiter_bytes(chunk_size=8192):
|
|
45
|
+
f.write(chunk)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Why
|
|
49
|
+
|
|
50
|
+
- `response.content` loads the entire response body into memory at once
|
|
51
|
+
- `client.stream()` reads the response incrementally — constant memory usage
|
|
52
|
+
- Essential for large file downloads, SSE streams, and NDJSON feeds
|
|
53
|
+
- `aiter_bytes()`, `aiter_lines()`, `aiter_text()` provide different iteration modes
|
|
54
|
+
|
|
55
|
+
## Streaming Iteration Methods
|
|
56
|
+
|
|
57
|
+
| Method | Use case |
|
|
58
|
+
|--------|---------|
|
|
59
|
+
| `aiter_bytes(chunk_size)` | Binary downloads, file writes |
|
|
60
|
+
| `aiter_lines()` | Line-delimited text (NDJSON, logs) |
|
|
61
|
+
| `aiter_text()` | Streamed text with encoding handling |
|
|
62
|
+
| `aiter_raw()` | Raw bytes without decompression |
|
|
63
|
+
|
|
64
|
+
## References
|
|
65
|
+
|
|
66
|
+
- [httpx Streaming Responses](https://www.python-httpx.org/async/#streaming-responses)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: pydantic-v2-config
|
|
3
|
+
title: Use model_config Instead of class Config
|
|
4
|
+
category: pydantic
|
|
5
|
+
layer: 2
|
|
6
|
+
tags:
|
|
7
|
+
- pydantic
|
|
8
|
+
- config
|
|
9
|
+
- migration
|
|
10
|
+
aliases:
|
|
11
|
+
- class Config
|
|
12
|
+
- model_config
|
|
13
|
+
- ConfigDict
|
|
14
|
+
python: ">=3.9"
|
|
15
|
+
frequency: high
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Use model_config Instead of class Config
|
|
19
|
+
|
|
20
|
+
Pydantic V2 replaces the inner `class Config` with a module-level `model_config` dict.
|
|
21
|
+
|
|
22
|
+
## BAD
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pydantic import BaseModel
|
|
26
|
+
|
|
27
|
+
class User(BaseModel):
|
|
28
|
+
name: str
|
|
29
|
+
email: str
|
|
30
|
+
|
|
31
|
+
class Config:
|
|
32
|
+
str_strip_whitespace = True
|
|
33
|
+
from_attributes = True
|
|
34
|
+
json_schema_extra = {"examples": [{"name": "Alice"}]}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## GOOD
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pydantic import BaseModel, ConfigDict
|
|
41
|
+
|
|
42
|
+
class User(BaseModel):
|
|
43
|
+
model_config = ConfigDict(
|
|
44
|
+
str_strip_whitespace=True,
|
|
45
|
+
from_attributes=True,
|
|
46
|
+
json_schema_extra={"examples": [{"name": "Alice"}]},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
email: str
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Why
|
|
54
|
+
|
|
55
|
+
- `class Config` is deprecated in Pydantic V2 (removed in V3)
|
|
56
|
+
- `ConfigDict` is typed — IDE autocompletion and type checking work
|
|
57
|
+
- Some V1 config keys were renamed (`orm_mode` → `from_attributes`, `allow_population_by_field_name` → `populate_by_name`)
|
|
58
|
+
- `model_config` is a class variable, not a nested class — simpler inheritance
|
|
59
|
+
|
|
60
|
+
## Migration Quick Reference
|
|
61
|
+
|
|
62
|
+
| V1 (class Config) | V2 (ConfigDict) |
|
|
63
|
+
|----|-----|
|
|
64
|
+
| `orm_mode = True` | `from_attributes=True` |
|
|
65
|
+
| `allow_population_by_field_name = True` | `populate_by_name=True` |
|
|
66
|
+
| `anystr_strip_whitespace = True` | `str_strip_whitespace=True` |
|
|
67
|
+
| `validate_assignment = True` | `validate_assignment=True` |
|
|
68
|
+
| `use_enum_values = True` | `use_enum_values=True` |
|
|
69
|
+
|
|
70
|
+
## References
|
|
71
|
+
|
|
72
|
+
- [Pydantic V2 Configuration](https://docs.pydantic.dev/latest/concepts/config/)
|
|
73
|
+
- [Pydantic V2 Migration Guide](https://docs.pydantic.dev/latest/migration/)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: pydantic-v2-model-api
|
|
3
|
+
title: Use Pydantic V2 model_validate Instead of parse_obj
|
|
4
|
+
category: pydantic
|
|
5
|
+
layer: 2
|
|
6
|
+
tags:
|
|
7
|
+
- pydantic
|
|
8
|
+
- validation
|
|
9
|
+
- migration
|
|
10
|
+
aliases:
|
|
11
|
+
- parse_obj
|
|
12
|
+
- parse_raw
|
|
13
|
+
- from_orm
|
|
14
|
+
- model_validate
|
|
15
|
+
python: ">=3.9"
|
|
16
|
+
frequency: high
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Use Pydantic V2 Model API
|
|
20
|
+
|
|
21
|
+
Pydantic V2 renamed core model methods. The V1 names are removed.
|
|
22
|
+
|
|
23
|
+
## BAD
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from pydantic import BaseModel
|
|
27
|
+
|
|
28
|
+
class User(BaseModel):
|
|
29
|
+
name: str
|
|
30
|
+
age: int
|
|
31
|
+
|
|
32
|
+
# V1 API (removed in V2)
|
|
33
|
+
user = User.parse_obj({"name": "Alice", "age": 30})
|
|
34
|
+
user = User.parse_raw('{"name": "Alice", "age": 30}')
|
|
35
|
+
user = User.from_orm(db_user)
|
|
36
|
+
data = user.dict()
|
|
37
|
+
json_str = user.json()
|
|
38
|
+
schema = User.schema()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## GOOD
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from pydantic import BaseModel
|
|
45
|
+
|
|
46
|
+
class User(BaseModel):
|
|
47
|
+
name: str
|
|
48
|
+
age: int
|
|
49
|
+
|
|
50
|
+
# V2 API
|
|
51
|
+
user = User.model_validate({"name": "Alice", "age": 30})
|
|
52
|
+
user = User.model_validate_json('{"name": "Alice", "age": 30}')
|
|
53
|
+
data = user.model_dump()
|
|
54
|
+
json_str = user.model_dump_json()
|
|
55
|
+
json_schema = User.model_json_schema()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Why
|
|
59
|
+
|
|
60
|
+
- V1 method names are removed in Pydantic V2
|
|
61
|
+
- `model_validate` is ~5-50x faster than V1's `parse_obj` (Rust core)
|
|
62
|
+
- `model_validate_json` parses JSON without intermediate dict
|
|
63
|
+
- Consistent `model_` prefix makes API discoverable
|
|
64
|
+
|
|
65
|
+
## Migration Quick Reference
|
|
66
|
+
|
|
67
|
+
| V1 | V2 |
|
|
68
|
+
|----|-----|
|
|
69
|
+
| `parse_obj(data)` | `model_validate(data)` |
|
|
70
|
+
| `parse_raw(json)` | `model_validate_json(json)` |
|
|
71
|
+
| `from_orm(obj)` | `model_validate(obj)` with `from_attributes=True` |
|
|
72
|
+
| `.dict()` | `.model_dump()` |
|
|
73
|
+
| `.json()` | `.model_dump_json()` |
|
|
74
|
+
| `.schema()` | `.model_json_schema()` |
|
|
75
|
+
| `.copy()` | `.model_copy()` |
|
|
76
|
+
|
|
77
|
+
## References
|
|
78
|
+
|
|
79
|
+
- [Pydantic V2 Migration Guide](https://docs.pydantic.dev/latest/migration/)
|
modern_python_guidance/skills/modern-python-guidance/guides/pydantic/pydantic-v2-serialization.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: pydantic-v2-serialization
|
|
3
|
+
title: Use field_serializer for Custom Serialization
|
|
4
|
+
category: pydantic
|
|
5
|
+
layer: 2
|
|
6
|
+
tags:
|
|
7
|
+
- pydantic
|
|
8
|
+
- serialization
|
|
9
|
+
- migration
|
|
10
|
+
aliases:
|
|
11
|
+
- field_serializer
|
|
12
|
+
- model_serializer
|
|
13
|
+
- json_encoders
|
|
14
|
+
python: ">=3.9"
|
|
15
|
+
frequency: medium
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Use field_serializer for Custom Serialization
|
|
19
|
+
|
|
20
|
+
Pydantic V2 replaces `json_encoders` in Config with `@field_serializer` and `@model_serializer` decorators.
|
|
21
|
+
|
|
22
|
+
## BAD
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from pydantic import BaseModel
|
|
27
|
+
|
|
28
|
+
class Event(BaseModel):
|
|
29
|
+
name: str
|
|
30
|
+
timestamp: datetime
|
|
31
|
+
|
|
32
|
+
class Config:
|
|
33
|
+
json_encoders = {
|
|
34
|
+
datetime: lambda v: v.isoformat(),
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## GOOD
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from datetime import datetime
|
|
42
|
+
from pydantic import BaseModel, field_serializer
|
|
43
|
+
|
|
44
|
+
class Event(BaseModel):
|
|
45
|
+
name: str
|
|
46
|
+
timestamp: datetime
|
|
47
|
+
|
|
48
|
+
@field_serializer("timestamp")
|
|
49
|
+
@classmethod
|
|
50
|
+
def serialize_timestamp(cls, v: datetime) -> str:
|
|
51
|
+
return v.isoformat()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Why
|
|
55
|
+
|
|
56
|
+
- `json_encoders` is removed in Pydantic V2
|
|
57
|
+
- `@field_serializer` is explicit about which fields it handles
|
|
58
|
+
- Type-safe — serializer input/output types are checked
|
|
59
|
+
- `@model_serializer` replaces whole-model custom serialization
|
|
60
|
+
- `mode="plain"` or `mode="wrap"` controls whether the default serializer runs first
|
|
61
|
+
|
|
62
|
+
## Serializer Modes
|
|
63
|
+
|
|
64
|
+
| Mode | Behavior |
|
|
65
|
+
|------|---------|
|
|
66
|
+
| `mode="plain"` (default) | Replaces the default serializer entirely |
|
|
67
|
+
| `mode="wrap"` | Receives the default serializer as a callable, can modify its output |
|
|
68
|
+
|
|
69
|
+
## References
|
|
70
|
+
|
|
71
|
+
- [Pydantic V2 Serialization](https://docs.pydantic.dev/latest/concepts/serialization/)
|
modern_python_guidance/skills/modern-python-guidance/guides/pydantic/pydantic-v2-validators.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: pydantic-v2-validators
|
|
3
|
+
title: Use Pydantic V2 field_validator Instead of validator
|
|
4
|
+
category: pydantic
|
|
5
|
+
layer: 2
|
|
6
|
+
tags:
|
|
7
|
+
- pydantic
|
|
8
|
+
- validation
|
|
9
|
+
- migration
|
|
10
|
+
aliases:
|
|
11
|
+
- validator
|
|
12
|
+
- field_validator
|
|
13
|
+
- model_validator
|
|
14
|
+
python: ">=3.9"
|
|
15
|
+
frequency: high
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Use Pydantic V2 Validators
|
|
19
|
+
|
|
20
|
+
Pydantic V2 replaced `@validator` and `@root_validator` with `@field_validator` and `@model_validator`.
|
|
21
|
+
|
|
22
|
+
## BAD
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pydantic import BaseModel, validator, root_validator
|
|
26
|
+
|
|
27
|
+
class User(BaseModel):
|
|
28
|
+
name: str
|
|
29
|
+
email: str
|
|
30
|
+
|
|
31
|
+
@validator("name")
|
|
32
|
+
@classmethod
|
|
33
|
+
def name_not_empty(cls, v):
|
|
34
|
+
if not v.strip():
|
|
35
|
+
raise ValueError("name cannot be empty")
|
|
36
|
+
return v.strip()
|
|
37
|
+
|
|
38
|
+
@root_validator
|
|
39
|
+
@classmethod
|
|
40
|
+
def check_consistency(cls, values):
|
|
41
|
+
return values
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## GOOD
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from pydantic import BaseModel, field_validator, model_validator
|
|
48
|
+
|
|
49
|
+
class User(BaseModel):
|
|
50
|
+
name: str
|
|
51
|
+
email: str
|
|
52
|
+
|
|
53
|
+
@field_validator("name")
|
|
54
|
+
@classmethod
|
|
55
|
+
def name_not_empty(cls, v: str) -> str:
|
|
56
|
+
if not v.strip():
|
|
57
|
+
raise ValueError("name cannot be empty")
|
|
58
|
+
return v.strip()
|
|
59
|
+
|
|
60
|
+
@model_validator(mode="after")
|
|
61
|
+
def check_consistency(self) -> "User":
|
|
62
|
+
return self
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Why
|
|
66
|
+
|
|
67
|
+
- `@validator` and `@root_validator` are deprecated in Pydantic V2 (removed in V3)
|
|
68
|
+
- `@field_validator` is explicit about which fields it validates
|
|
69
|
+
- `@model_validator(mode="before"|"after")` replaces `@root_validator(pre=True|False)`
|
|
70
|
+
- `mode="after"` receives the model instance, not a raw dict
|
|
71
|
+
|
|
72
|
+
## Migration Quick Reference
|
|
73
|
+
|
|
74
|
+
| V1 | V2 |
|
|
75
|
+
|----|-----|
|
|
76
|
+
| `@validator("field")` | `@field_validator("field")` |
|
|
77
|
+
| `@validator("field", pre=True)` | `@field_validator("field", mode="before")` |
|
|
78
|
+
| `@root_validator` | `@model_validator(mode="after")` |
|
|
79
|
+
| `@root_validator(pre=True)` | `@model_validator(mode="before")` |
|
|
80
|
+
|
|
81
|
+
## References
|
|
82
|
+
|
|
83
|
+
- [Pydantic V2 Validators](https://docs.pydantic.dev/latest/concepts/validators/)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: datetime-utc
|
|
3
|
+
title: Use datetime.now(UTC) Instead of utcnow()
|
|
4
|
+
category: stdlib
|
|
5
|
+
layer: 1
|
|
6
|
+
tags:
|
|
7
|
+
- datetime
|
|
8
|
+
- timezone
|
|
9
|
+
- utc
|
|
10
|
+
aliases:
|
|
11
|
+
- utcnow
|
|
12
|
+
- datetime.utcnow
|
|
13
|
+
- datetime.utcfromtimestamp
|
|
14
|
+
python: ">=3.11"
|
|
15
|
+
frequency: high
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Use datetime.now(UTC) Instead of utcnow()
|
|
19
|
+
|
|
20
|
+
`datetime.utcnow()` returns a naive datetime (no timezone info). This is a common source of bugs. Use `datetime.now(UTC)` for timezone-aware UTC datetimes.
|
|
21
|
+
|
|
22
|
+
## BAD
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
|
|
27
|
+
now = datetime.utcnow()
|
|
28
|
+
ts = datetime.utcfromtimestamp(1234567890)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## GOOD
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from datetime import UTC, datetime
|
|
35
|
+
|
|
36
|
+
now = datetime.now(UTC)
|
|
37
|
+
ts = datetime.fromtimestamp(1234567890, tz=UTC)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Why
|
|
41
|
+
|
|
42
|
+
- `utcnow()` and `utcfromtimestamp()` are deprecated since Python 3.12
|
|
43
|
+
- Naive datetimes cause subtle bugs in timezone arithmetic
|
|
44
|
+
- `datetime.now(UTC)` returns a proper timezone-aware datetime
|
|
45
|
+
- The `UTC` singleton was added in Python 3.11
|
|
46
|
+
|
|
47
|
+
## Version Notes
|
|
48
|
+
|
|
49
|
+
- 3.11+: `datetime.UTC` constant available
|
|
50
|
+
- 3.12+: `utcnow()` and `utcfromtimestamp()` emit `DeprecationWarning`
|
|
51
|
+
- For 3.9-3.10: use `datetime.now(timezone.utc)` instead
|
|
52
|
+
|
|
53
|
+
## References
|
|
54
|
+
|
|
55
|
+
- [datetime.UTC docs](https://docs.python.org/3/library/datetime.html#datetime.UTC)
|
|
56
|
+
- [What's New in Python 3.12 — Deprecations](https://docs.python.org/3/whatsnew/3.12.html)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: pathlib-over-os-path
|
|
3
|
+
title: Use pathlib.Path Instead of os.path
|
|
4
|
+
category: stdlib
|
|
5
|
+
layer: 1
|
|
6
|
+
tags:
|
|
7
|
+
- pathlib
|
|
8
|
+
- filesystem
|
|
9
|
+
- os.path
|
|
10
|
+
aliases:
|
|
11
|
+
- os.path
|
|
12
|
+
- os.path.join
|
|
13
|
+
- os.path.exists
|
|
14
|
+
python: ">=3.9"
|
|
15
|
+
frequency: high
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Use pathlib.Path Instead of os.path
|
|
19
|
+
|
|
20
|
+
Use `pathlib.Path` for filesystem operations instead of string-based `os.path` functions.
|
|
21
|
+
|
|
22
|
+
## BAD
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import os
|
|
26
|
+
|
|
27
|
+
config_path = os.path.join(os.path.expanduser("~"), ".config", "app", "config.toml")
|
|
28
|
+
if os.path.exists(config_path):
|
|
29
|
+
with open(config_path) as f:
|
|
30
|
+
data = f.read()
|
|
31
|
+
|
|
32
|
+
parent = os.path.dirname(config_path)
|
|
33
|
+
name = os.path.basename(config_path)
|
|
34
|
+
ext = os.path.splitext(config_path)[1]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## GOOD
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
config_path = Path.home() / ".config" / "app" / "config.toml"
|
|
43
|
+
if config_path.exists():
|
|
44
|
+
data = config_path.read_text()
|
|
45
|
+
|
|
46
|
+
parent = config_path.parent
|
|
47
|
+
name = config_path.name
|
|
48
|
+
ext = config_path.suffix
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Why
|
|
52
|
+
|
|
53
|
+
- `/` operator for path joining is more readable than `os.path.join`
|
|
54
|
+
- Methods on Path objects instead of free functions on strings
|
|
55
|
+
- Built-in `read_text()`, `write_text()`, `read_bytes()`, `write_bytes()`
|
|
56
|
+
- Type safety — `Path` vs raw `str` catches path/string confusion
|
|
57
|
+
- `os.path` functions still work with `Path` objects (backwards compatible)
|
|
58
|
+
|
|
59
|
+
## Version Notes
|
|
60
|
+
|
|
61
|
+
- 3.4+: `pathlib.Path` available
|
|
62
|
+
- 3.6+: `os` functions accept `Path` objects
|
|
63
|
+
- 3.9+: `Path.with_stem()`, `Path.is_relative_to()`
|
|
64
|
+
- 3.12+: `Path.walk()` replaces `os.walk()`
|
|
65
|
+
|
|
66
|
+
## References
|
|
67
|
+
|
|
68
|
+
- [pathlib documentation](https://docs.python.org/3/library/pathlib.html)
|
modern_python_guidance/skills/modern-python-guidance/guides/stdlib/removeprefix-removesuffix.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: removeprefix-removesuffix
|
|
3
|
+
title: Use str.removeprefix/removesuffix Instead of Slicing
|
|
4
|
+
category: stdlib
|
|
5
|
+
layer: 1
|
|
6
|
+
tags:
|
|
7
|
+
- string
|
|
8
|
+
- stdlib
|
|
9
|
+
aliases:
|
|
10
|
+
- removeprefix
|
|
11
|
+
- removesuffix
|
|
12
|
+
- lstrip
|
|
13
|
+
- rstrip
|
|
14
|
+
python: ">=3.9"
|
|
15
|
+
frequency: medium
|
|
16
|
+
pep: 616
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Use str.removeprefix/removesuffix
|
|
20
|
+
|
|
21
|
+
Since Python 3.9, use `str.removeprefix()` and `str.removesuffix()` instead of manual slicing or `lstrip()`/`rstrip()`.
|
|
22
|
+
|
|
23
|
+
## BAD
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
filename = "test_utils.py"
|
|
27
|
+
|
|
28
|
+
# Manual slicing — must know exact length
|
|
29
|
+
if filename.startswith("test_"):
|
|
30
|
+
name = filename[5:] # fragile: magic number 5
|
|
31
|
+
|
|
32
|
+
# Common mistake: lstrip removes CHARACTERS, not prefix
|
|
33
|
+
name = filename.lstrip("test_") # removes t, e, s, _, not "test_"
|
|
34
|
+
# "test_utils.py".lstrip("test_") == "utils.py" (lucky)
|
|
35
|
+
# "test_test.py".lstrip("test_") == ".py" (wrong!)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## GOOD
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
filename = "test_utils.py"
|
|
42
|
+
|
|
43
|
+
name = filename.removeprefix("test_") # "utils.py"
|
|
44
|
+
base = filename.removesuffix(".py") # "test_utils"
|
|
45
|
+
|
|
46
|
+
# Safe: returns original string if prefix/suffix not found
|
|
47
|
+
name = "production.py".removeprefix("test_") # "production.py"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Why
|
|
51
|
+
|
|
52
|
+
- No magic numbers or `len()` calculations
|
|
53
|
+
- Semantically clear — removes a specific string, not individual characters
|
|
54
|
+
- Safe — returns the original string unchanged if prefix/suffix not present
|
|
55
|
+
- Avoids the `lstrip`/`rstrip` character-set trap
|
|
56
|
+
|
|
57
|
+
## Version Notes
|
|
58
|
+
|
|
59
|
+
- 3.9+: `str.removeprefix()`, `str.removesuffix()`
|
|
60
|
+
- Also available on `bytes` and `bytearray`
|
|
61
|
+
|
|
62
|
+
## References
|
|
63
|
+
|
|
64
|
+
- [PEP 616 — String methods to remove prefixes and suffixes](https://peps.python.org/pep-0616/)
|