@coralai/sps-cli 0.42.0 → 0.44.0

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 (147) hide show
  1. package/README.md +59 -4
  2. package/dist/commands/consoleCommand.d.ts +2 -0
  3. package/dist/commands/consoleCommand.d.ts.map +1 -0
  4. package/dist/commands/consoleCommand.js +129 -0
  5. package/dist/commands/consoleCommand.js.map +1 -0
  6. package/dist/commands/projectInit.d.ts.map +1 -1
  7. package/dist/commands/projectInit.js +40 -53
  8. package/dist/commands/projectInit.js.map +1 -1
  9. package/dist/commands/setup.d.ts.map +1 -1
  10. package/dist/commands/setup.js +14 -2
  11. package/dist/commands/setup.js.map +1 -1
  12. package/dist/commands/skillCommand.d.ts +2 -0
  13. package/dist/commands/skillCommand.d.ts.map +1 -0
  14. package/dist/commands/skillCommand.js +235 -0
  15. package/dist/commands/skillCommand.js.map +1 -0
  16. package/dist/console-assets/assets/index-Bhd2f9AP.js +125 -0
  17. package/dist/console-assets/assets/index-bsAN2a12.css +1 -0
  18. package/dist/console-assets/index.html +16 -0
  19. package/dist/console-server/index.d.ts +29 -0
  20. package/dist/console-server/index.d.ts.map +1 -0
  21. package/dist/console-server/index.js +145 -0
  22. package/dist/console-server/index.js.map +1 -0
  23. package/dist/console-server/lib/lockFile.d.ts +17 -0
  24. package/dist/console-server/lib/lockFile.d.ts.map +1 -0
  25. package/dist/console-server/lib/lockFile.js +61 -0
  26. package/dist/console-server/lib/lockFile.js.map +1 -0
  27. package/dist/console-server/lib/portPicker.d.ts +3 -0
  28. package/dist/console-server/lib/portPicker.d.ts.map +1 -0
  29. package/dist/console-server/lib/portPicker.js +25 -0
  30. package/dist/console-server/lib/portPicker.js.map +1 -0
  31. package/dist/console-server/routes/projects.d.ts +11 -0
  32. package/dist/console-server/routes/projects.d.ts.map +1 -0
  33. package/dist/console-server/routes/projects.js +149 -0
  34. package/dist/console-server/routes/projects.js.map +1 -0
  35. package/dist/console-server/routes/system.d.ts +7 -0
  36. package/dist/console-server/routes/system.d.ts.map +1 -0
  37. package/dist/console-server/routes/system.js +19 -0
  38. package/dist/console-server/routes/system.js.map +1 -0
  39. package/dist/console-server/sse/eventBus.d.ts +25 -0
  40. package/dist/console-server/sse/eventBus.d.ts.map +1 -0
  41. package/dist/console-server/sse/eventBus.js +32 -0
  42. package/dist/console-server/sse/eventBus.js.map +1 -0
  43. package/dist/console-server/watchers/cardWatcher.d.ts +9 -0
  44. package/dist/console-server/watchers/cardWatcher.d.ts.map +1 -0
  45. package/dist/console-server/watchers/cardWatcher.js +42 -0
  46. package/dist/console-server/watchers/cardWatcher.js.map +1 -0
  47. package/dist/core/skillStore.d.ts +46 -0
  48. package/dist/core/skillStore.d.ts.map +1 -0
  49. package/dist/core/skillStore.js +210 -0
  50. package/dist/core/skillStore.js.map +1 -0
  51. package/dist/core/skillStore.test.d.ts +2 -0
  52. package/dist/core/skillStore.test.d.ts.map +1 -0
  53. package/dist/core/skillStore.test.js +203 -0
  54. package/dist/core/skillStore.test.js.map +1 -0
  55. package/dist/main.js +27 -17
  56. package/dist/main.js.map +1 -1
  57. package/package.json +8 -2
  58. package/skills/architecture-decision-records/SKILL.md +207 -0
  59. package/skills/backend/SKILL.md +62 -0
  60. package/skills/backend/references/api-design.md +168 -0
  61. package/skills/backend/references/caching.md +181 -0
  62. package/skills/backend/references/data-access.md +173 -0
  63. package/skills/backend/references/layering.md +181 -0
  64. package/skills/backend/references/observability.md +190 -0
  65. package/skills/backend/references/resilience.md +201 -0
  66. package/skills/backend/references/security.md +186 -0
  67. package/skills/backend-architect/SKILL.md +119 -0
  68. package/skills/code-reviewer/SKILL.md +143 -0
  69. package/skills/coding-standards/SKILL.md +60 -0
  70. package/skills/coding-standards/references/clean-code.md +258 -0
  71. package/skills/coding-standards/references/code-review.md +192 -0
  72. package/skills/coding-standards/references/commits-and-prs.md +226 -0
  73. package/skills/coding-standards/references/error-strategy.md +193 -0
  74. package/skills/coding-standards/references/naming.md +185 -0
  75. package/skills/coding-standards/references/tdd.md +171 -0
  76. package/skills/database/SKILL.md +53 -0
  77. package/skills/database/references/indexing.md +190 -0
  78. package/skills/database/references/migrations.md +199 -0
  79. package/skills/database/references/nosql.md +185 -0
  80. package/skills/database/references/queries.md +295 -0
  81. package/skills/database/references/scaling.md +203 -0
  82. package/skills/database/references/schema.md +191 -0
  83. package/skills/database-optimizer/SKILL.md +168 -0
  84. package/skills/debugging-workflow/SKILL.md +244 -0
  85. package/skills/devops/SKILL.md +55 -0
  86. package/skills/devops/references/ci-cd.md +204 -0
  87. package/skills/devops/references/containers.md +272 -0
  88. package/skills/devops/references/deploy.md +201 -0
  89. package/skills/devops/references/iac.md +252 -0
  90. package/skills/devops/references/observability.md +228 -0
  91. package/skills/devops/references/secrets.md +178 -0
  92. package/skills/devops-automator/SKILL.md +164 -0
  93. package/skills/frontend/SKILL.md +52 -0
  94. package/skills/frontend/references/accessibility.md +222 -0
  95. package/skills/frontend/references/components.md +206 -0
  96. package/skills/frontend/references/performance.md +219 -0
  97. package/skills/frontend/references/routing.md +209 -0
  98. package/skills/frontend/references/state.md +190 -0
  99. package/skills/frontend/references/testing.md +216 -0
  100. package/skills/frontend-developer/SKILL.md +115 -0
  101. package/skills/git-workflow/SKILL.md +355 -0
  102. package/skills/golang/SKILL.md +49 -0
  103. package/skills/golang/references/concurrency.md +284 -0
  104. package/skills/golang/references/errors.md +241 -0
  105. package/skills/golang/references/idioms.md +285 -0
  106. package/skills/golang/references/testing.md +238 -0
  107. package/skills/java/SKILL.md +50 -0
  108. package/skills/java/references/concurrency.md +194 -0
  109. package/skills/java/references/idioms.md +283 -0
  110. package/skills/java/references/testing.md +228 -0
  111. package/skills/kotlin/SKILL.md +47 -0
  112. package/skills/kotlin/references/coroutines.md +240 -0
  113. package/skills/kotlin/references/idioms.md +268 -0
  114. package/skills/kotlin/references/testing.md +219 -0
  115. package/skills/mobile/SKILL.md +50 -0
  116. package/skills/mobile/references/architecture.md +204 -0
  117. package/skills/mobile/references/navigation.md +158 -0
  118. package/skills/mobile/references/performance.md +152 -0
  119. package/skills/mobile/references/platform.md +166 -0
  120. package/skills/mobile/references/state-and-data.md +174 -0
  121. package/skills/python/SKILL.md +51 -0
  122. package/skills/python/THIRD_PARTY.md +14 -0
  123. package/skills/python/references/async.md +218 -0
  124. package/skills/python/references/error-handling.md +254 -0
  125. package/skills/python/references/idioms.md +279 -0
  126. package/skills/python/references/packaging.md +233 -0
  127. package/skills/python/references/testing.md +269 -0
  128. package/skills/python/references/typing.md +292 -0
  129. package/skills/qa-tester/SKILL.md +186 -0
  130. package/skills/rust/SKILL.md +50 -0
  131. package/skills/rust/references/async.md +224 -0
  132. package/skills/rust/references/errors.md +240 -0
  133. package/skills/rust/references/ownership.md +263 -0
  134. package/skills/rust/references/testing.md +274 -0
  135. package/skills/rust/references/traits.md +250 -0
  136. package/skills/security-engineer/SKILL.md +157 -0
  137. package/skills/swift/SKILL.md +48 -0
  138. package/skills/swift/references/concurrency.md +280 -0
  139. package/skills/swift/references/idioms.md +334 -0
  140. package/skills/swift/references/testing.md +229 -0
  141. package/skills/typescript/SKILL.md +51 -0
  142. package/skills/typescript/references/async.md +241 -0
  143. package/skills/typescript/references/errors.md +208 -0
  144. package/skills/typescript/references/idioms.md +246 -0
  145. package/skills/typescript/references/testing.md +225 -0
  146. package/skills/typescript/references/tooling.md +208 -0
  147. package/skills/typescript/references/types.md +259 -0
@@ -0,0 +1,269 @@
1
+ # Python Testing
2
+
3
+ pytest-specific patterns. For TDD cycle and general test-driven methodology, see the `coding-standards` skill (`references/tdd.md`).
4
+
5
+ ## Coverage targets
6
+
7
+ | Layer | Target | Rationale |
8
+ |---|---|---|
9
+ | Unit (pure logic) | ≥ 90% | Fast, cheap, high signal |
10
+ | Integration | ≥ 70% | Covers real dependencies (DB, cache, HTTP) |
11
+ | E2E | Key flows only | Slow, brittle — don't overinvest |
12
+
13
+ Use `pytest-cov`:
14
+ ```bash
15
+ pytest --cov=myapp --cov-report=term-missing --cov-fail-under=80
16
+ ```
17
+
18
+ ## pytest structure
19
+
20
+ File and function naming:
21
+ - Files: `test_*.py` or `*_test.py`
22
+ - Test functions: `test_*`
23
+ - Classes (optional grouping): `class TestFoo`
24
+
25
+ ```python
26
+ # test_user_service.py
27
+ def test_create_user_returns_user_object():
28
+ service = UserService(db=FakeDB())
29
+ user = service.create(name="Alice", email="a@x.com")
30
+ assert user.name == "Alice"
31
+ assert user.id is not None
32
+
33
+ def test_create_user_rejects_empty_email():
34
+ service = UserService(db=FakeDB())
35
+ with pytest.raises(ValidationError, match="email"):
36
+ service.create(name="Alice", email="")
37
+ ```
38
+
39
+ ## Assertions: prefer plain `assert`
40
+
41
+ pytest rewrites `assert` to show helpful diffs. Never use `self.assertEqual` (that's unittest).
42
+
43
+ ```python
44
+ # Good
45
+ assert result == expected
46
+ assert user in users
47
+ assert "error" in str(caplog.records[0])
48
+ assert 0 < percent <= 100
49
+
50
+ # Bad
51
+ self.assertEqual(result, expected) # unittest style — ugly, no advantage
52
+ assertTrue(result == expected) # ditto
53
+ ```
54
+
55
+ ## Fixtures
56
+
57
+ Use fixtures for setup/teardown, not manual init in every test.
58
+
59
+ ```python
60
+ import pytest
61
+
62
+ @pytest.fixture
63
+ def user_service():
64
+ service = UserService(db=FakeDB())
65
+ yield service
66
+ service.cleanup()
67
+
68
+ def test_create_user(user_service):
69
+ user = user_service.create(name="A", email="a@x.com")
70
+ assert user.id
71
+ ```
72
+
73
+ Scope levels (reuse across tests):
74
+ - `function` (default): new instance per test
75
+ - `class`: shared within a class
76
+ - `module`: shared within a file
77
+ - `session`: shared across entire run
78
+
79
+ ```python
80
+ @pytest.fixture(scope="session")
81
+ def db_connection():
82
+ conn = connect_to_test_db()
83
+ yield conn
84
+ conn.close()
85
+ ```
86
+
87
+ **Shared fixtures**: put in `conftest.py` in the test directory.
88
+
89
+ ## Parametrization
90
+
91
+ Same test logic, multiple inputs — one line per case.
92
+
93
+ ```python
94
+ @pytest.mark.parametrize("a,b,expected", [
95
+ (1, 2, 3),
96
+ (0, 0, 0),
97
+ (-1, 1, 0),
98
+ (100, 200, 300),
99
+ ])
100
+ def test_add(a, b, expected):
101
+ assert add(a, b) == expected
102
+
103
+ # With IDs for readable output
104
+ @pytest.mark.parametrize("input,expected", [
105
+ ("hello", 5),
106
+ ("", 0),
107
+ ("a b c", 5),
108
+ ], ids=["simple", "empty", "with_spaces"])
109
+ def test_len(input, expected):
110
+ assert len(input) == expected
111
+ ```
112
+
113
+ ## Mocking
114
+
115
+ Use `pytest-mock` (`mocker` fixture) or `unittest.mock`.
116
+
117
+ ```python
118
+ def test_sends_email_on_signup(mocker):
119
+ mock_send = mocker.patch('myapp.email.send')
120
+ service.signup(email="a@x.com")
121
+ mock_send.assert_called_once_with(to="a@x.com", template="welcome")
122
+
123
+ def test_handles_api_timeout(mocker):
124
+ mocker.patch('myapp.http.get', side_effect=TimeoutError)
125
+ result = service.fetch_data()
126
+ assert result.error == "timeout"
127
+ ```
128
+
129
+ ### Mock rules
130
+
131
+ | Rule | Why |
132
+ |---|---|
133
+ | Patch where it's USED, not where it's DEFINED | `patch('myapp.service.http.get')` not `patch('requests.get')` |
134
+ | Use `autospec=True` for stricter signatures | Catches "mock called with wrong args" |
135
+ | Don't over-mock | If you mock 8 things to test 10 lines, something's wrong |
136
+ | Prefer fakes over mocks | A `FakeDB` that actually works is easier to maintain |
137
+
138
+ ## Markers
139
+
140
+ Mark tests for selective running.
141
+
142
+ ```python
143
+ @pytest.mark.slow
144
+ def test_full_import_pipeline():
145
+ ... # takes 30s
146
+
147
+ @pytest.mark.integration
148
+ def test_with_real_db():
149
+ ...
150
+
151
+ # Run only fast tests:
152
+ # pytest -m "not slow"
153
+
154
+ # Run only integration:
155
+ # pytest -m integration
156
+ ```
157
+
158
+ Register markers in `pyproject.toml` to avoid warnings:
159
+ ```toml
160
+ [tool.pytest.ini_options]
161
+ markers = [
162
+ "slow: tests taking more than 1 second",
163
+ "integration: tests requiring external services",
164
+ ]
165
+ ```
166
+
167
+ ## Testing async code
168
+
169
+ Use `pytest-asyncio`.
170
+
171
+ ```python
172
+ import pytest
173
+
174
+ @pytest.mark.asyncio
175
+ async def test_fetch_url():
176
+ result = await fetch("https://example.com")
177
+ assert result.status == 200
178
+ ```
179
+
180
+ For the async language-level patterns being tested, see `references/async.md`.
181
+
182
+ ## Property-based testing with Hypothesis
183
+
184
+ When the input space is large (parsers, serializers, invariants), `hypothesis` generates inputs automatically and shrinks failing cases.
185
+
186
+ ```python
187
+ from hypothesis import given, strategies as st
188
+
189
+ @given(st.lists(st.integers()))
190
+ def test_sort_is_idempotent(xs):
191
+ assert sorted(sorted(xs)) == sorted(xs)
192
+
193
+ @given(st.text())
194
+ def test_roundtrip(s):
195
+ assert decode(encode(s)) == s
196
+
197
+ # Composite strategy
198
+ user_strategy = st.builds(
199
+ User,
200
+ id=st.uuids().map(str),
201
+ age=st.integers(min_value=0, max_value=150),
202
+ email=st.emails(),
203
+ )
204
+
205
+ @given(user_strategy)
206
+ def test_user_serializes(u):
207
+ assert User.from_json(u.to_json()) == u
208
+ ```
209
+
210
+ Use it where example-based tests are weak: round-trips, invariants, mathematical properties, parsers.
211
+
212
+ ## Testing exceptions
213
+
214
+ ```python
215
+ def test_raises_on_invalid_input():
216
+ with pytest.raises(ValidationError, match="email required"):
217
+ validate_user({})
218
+
219
+ def test_exception_chain():
220
+ with pytest.raises(ConfigError) as exc_info:
221
+ load_config("/nonexistent")
222
+ assert isinstance(exc_info.value.__cause__, FileNotFoundError)
223
+ ```
224
+
225
+ ## Anti-patterns
226
+
227
+ | Anti-pattern | Fix |
228
+ |---|---|
229
+ | Shared mutable state across tests | Use fixtures with function scope |
230
+ | `if __name__ == "__main__": pytest.main()` in tests | Run via `pytest` CLI |
231
+ | Print statements for debugging | Use `caplog` fixture or `-s` flag |
232
+ | Testing private methods | Test via public API; if you can't, refactor |
233
+ | One big `test_everything()` | Split: one behavior per test |
234
+ | Skipping tests without comment | `@pytest.mark.skip("reason")` always with reason |
235
+
236
+ ## Test organization
237
+
238
+ ```
239
+ project/
240
+ ├── src/myapp/
241
+ │ ├── services/
242
+ │ │ └── user_service.py
243
+ │ └── models/
244
+ │ └── user.py
245
+ └── tests/
246
+ ├── conftest.py # shared fixtures
247
+ ├── unit/
248
+ │ ├── test_user_service.py
249
+ │ └── test_user_model.py
250
+ ├── integration/
251
+ │ └── test_user_api.py
252
+ └── e2e/
253
+ └── test_signup_flow.py
254
+ ```
255
+
256
+ ## CI setup
257
+
258
+ ```toml
259
+ # pyproject.toml
260
+ [tool.pytest.ini_options]
261
+ addopts = [
262
+ "--cov=myapp",
263
+ "--cov-report=term-missing",
264
+ "--cov-fail-under=80",
265
+ "--strict-markers",
266
+ "-ra", # show summary for all non-pass outcomes
267
+ ]
268
+ testpaths = ["tests"]
269
+ ```
@@ -0,0 +1,292 @@
1
+ # Python Type Hints
2
+
3
+ Modern typing for public APIs. Python 3.9+ syntax preferred.
4
+
5
+ ## Baseline: type all public APIs
6
+
7
+ Every public function, method, class attribute gets a type hint. Private helpers can skip if the type is obvious from context.
8
+
9
+ ```python
10
+ def process_user(
11
+ user_id: str,
12
+ data: dict[str, Any],
13
+ active: bool = True,
14
+ ) -> User | None:
15
+ """Update a user. Returns the updated User or None if inactive."""
16
+ if not active:
17
+ return None
18
+ return User(user_id, data)
19
+ ```
20
+
21
+ ## Modern syntax (Python 3.9+)
22
+
23
+ Use built-in generics, drop `typing.List` / `typing.Dict` imports.
24
+
25
+ ```python
26
+ # Python 3.9+ ✅
27
+ def process(items: list[str]) -> dict[str, int]:
28
+ return {item: len(item) for item in items}
29
+
30
+ # Python 3.8 and earlier — legacy only
31
+ from typing import List, Dict
32
+ def process(items: List[str]) -> Dict[str, int]: ...
33
+ ```
34
+
35
+ ## Optional / Union
36
+
37
+ Python 3.10+: use `|` instead of `Union` / `Optional`.
38
+
39
+ ```python
40
+ # Python 3.10+ ✅
41
+ def find(id: str) -> User | None: ...
42
+ def parse(raw: str | bytes) -> dict: ...
43
+
44
+ # Python 3.9 and earlier
45
+ from typing import Optional, Union
46
+ def find(id: str) -> Optional[User]: ...
47
+ def parse(raw: Union[str, bytes]) -> dict: ...
48
+ ```
49
+
50
+ ## Type aliases
51
+
52
+ Name complex types. Makes signatures self-documenting.
53
+
54
+ ```python
55
+ from typing import TypeAlias
56
+
57
+ JSON: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None
58
+ UserId: TypeAlias = str
59
+ RequestHeaders: TypeAlias = dict[str, str]
60
+
61
+ def fetch(user_id: UserId, headers: RequestHeaders) -> JSON: ...
62
+ ```
63
+
64
+ ## Generics
65
+
66
+ For functions that work over any type while preserving relationships.
67
+
68
+ ### Python 3.12+: PEP 695 type parameter syntax
69
+
70
+ Inline declaration, no `TypeVar` import needed.
71
+
72
+ ```python
73
+ # Generic function
74
+ def first[T](items: list[T]) -> T | None:
75
+ return items[0] if items else None
76
+
77
+ # Bounded type parameter
78
+ def clamp[N: (int, float)](value: N, lo: N, hi: N) -> N:
79
+ return max(lo, min(value, hi))
80
+
81
+ # Generic class
82
+ class Stack[T]:
83
+ def __init__(self) -> None:
84
+ self._items: list[T] = []
85
+ def push(self, item: T) -> None: self._items.append(item)
86
+ def pop(self) -> T: return self._items.pop()
87
+
88
+ # Type alias (PEP 695)
89
+ type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
90
+ type UserId = str
91
+ ```
92
+
93
+ ### Python 3.11 and earlier: `TypeVar`
94
+
95
+ ```python
96
+ from typing import TypeVar
97
+
98
+ T = TypeVar('T')
99
+
100
+ def first(items: list[T]) -> T | None:
101
+ return items[0] if items else None
102
+
103
+ N = TypeVar('N', bound=int | float)
104
+
105
+ def clamp(value: N, lo: N, hi: N) -> N:
106
+ return max(lo, min(value, hi))
107
+ ```
108
+
109
+ ## `Self` type (Python 3.11+)
110
+
111
+ Reference the enclosing class without string forward references.
112
+
113
+ ```python
114
+ from typing import Self
115
+
116
+ class QueryBuilder:
117
+ def where(self, **kwargs) -> Self:
118
+ self._filters.update(kwargs)
119
+ return self
120
+ def limit(self, n: int) -> Self:
121
+ self._limit = n
122
+ return self
123
+
124
+ # Subclass chaining preserves the subclass type
125
+ class UserQuery(QueryBuilder):
126
+ def active(self) -> Self:
127
+ return self.where(active=True)
128
+
129
+ UserQuery().active().limit(10) # still typed as UserQuery, not QueryBuilder
130
+ ```
131
+
132
+ ## `@override` decorator (Python 3.12+)
133
+
134
+ Catch accidental method-name drift when the parent renames.
135
+
136
+ ```python
137
+ from typing import override
138
+
139
+ class Base:
140
+ def process(self) -> None: ...
141
+
142
+ class Derived(Base):
143
+ @override
144
+ def process(self) -> None: ... # type-checked: errors if Base.process is renamed or gone
145
+
146
+ @override
147
+ def procces(self) -> None: ... # ❌ mypy/pyright error: not overriding anything
148
+ ```
149
+
150
+ ## `ParamSpec` — typed decorators
151
+
152
+ Preserve the signature of the wrapped function.
153
+
154
+ ```python
155
+ from typing import ParamSpec, TypeVar, Callable
156
+ from functools import wraps
157
+
158
+ P = ParamSpec('P')
159
+ R = TypeVar('R')
160
+
161
+ def timed(func: Callable[P, R]) -> Callable[P, R]:
162
+ @wraps(func)
163
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
164
+ return func(*args, **kwargs)
165
+ return wrapper
166
+
167
+ # Python 3.12+ syntax
168
+ def timed[**P, R](func: Callable[P, R]) -> Callable[P, R]:
169
+ ...
170
+ ```
171
+
172
+ ## Protocols — structural typing
173
+
174
+ Interface without inheritance. Works like Go interfaces or TypeScript interfaces.
175
+
176
+ ```python
177
+ from typing import Protocol
178
+
179
+ class Renderable(Protocol):
180
+ def render(self) -> str: ...
181
+
182
+ # Any class with a matching .render() method satisfies Renderable —
183
+ # no need to inherit from it.
184
+ class Card:
185
+ def render(self) -> str:
186
+ return f"Card: {self.title}"
187
+
188
+ def render_all(items: list[Renderable]) -> str:
189
+ return "\n".join(item.render() for item in items)
190
+
191
+ # Works without any declaration:
192
+ render_all([Card("A"), Card("B")])
193
+ ```
194
+
195
+ ## Literal types
196
+
197
+ Constrain values to a fixed set.
198
+
199
+ ```python
200
+ from typing import Literal
201
+
202
+ LogLevel = Literal['debug', 'info', 'warning', 'error', 'critical']
203
+
204
+ def log(msg: str, level: LogLevel = 'info') -> None: ...
205
+
206
+ log("x", "info") # ✅
207
+ log("x", "verbose") # ❌ type error
208
+ ```
209
+
210
+ ## TypedDict — structured dicts
211
+
212
+ When you're stuck with dict but want type safety.
213
+
214
+ ```python
215
+ from typing import TypedDict
216
+
217
+ class UserDict(TypedDict):
218
+ id: str
219
+ name: str
220
+ email: str
221
+ active: bool
222
+
223
+ def process(user: UserDict) -> None:
224
+ print(user['name']) # type-checked as str
225
+ ```
226
+
227
+ ## Class attributes: `ClassVar` vs `Final`
228
+
229
+ ```python
230
+ from typing import ClassVar, Final
231
+ from dataclasses import dataclass
232
+
233
+ @dataclass
234
+ class Config:
235
+ url: str # instance attr
236
+ timeout: int = 30 # instance attr with default
237
+ VERSION: ClassVar[str] = '1.0' # class-level constant
238
+ MAX_RETRIES: Final[int] = 3 # cannot be reassigned
239
+ ```
240
+
241
+ ## Callable types
242
+
243
+ Types for function parameters.
244
+
245
+ ```python
246
+ from typing import Callable
247
+
248
+ # Callable[[arg_types...], return_type]
249
+ def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
250
+ return func(a, b)
251
+
252
+ apply(lambda x, y: x + y, 2, 3) # 5
253
+ ```
254
+
255
+ ## When to skip types
256
+
257
+ Private one-liners where the type is obvious:
258
+ ```python
259
+ # Fine — _hash is clearly int from hashlib
260
+ def _hash(s): return hashlib.sha256(s.encode()).hexdigest()
261
+ ```
262
+
263
+ Test functions: types are usually redundant if the test body makes them obvious.
264
+
265
+ ## Runtime type checking
266
+
267
+ Most type hints are **not** enforced at runtime. Use `mypy` or `pyright` in CI.
268
+
269
+ ```bash
270
+ # mypy.ini
271
+ [mypy]
272
+ strict = true
273
+ python_version = 3.11
274
+ warn_return_any = true
275
+ warn_unused_ignores = true
276
+ ```
277
+
278
+ For runtime validation at API boundaries, use `pydantic` or `attrs` — not type hints.
279
+
280
+ ## Type ignore comments
281
+
282
+ Use sparingly. Narrow the suppression to the exact issue.
283
+
284
+ ```python
285
+ # ignore one specific line
286
+ result = legacy_api() # type: ignore[no-untyped-call]
287
+
288
+ # for whole file (top of file, last resort)
289
+ # type: ignore
290
+ ```
291
+
292
+ Never bare `# type: ignore` with no specific code. Reviewers can't tell what was suppressed.