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,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/)
@@ -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/)
@@ -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)
@@ -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/)