@coralai/sps-cli 0.41.2 → 0.43.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.
- package/README.md +34 -3
- package/dist/commands/cardAdd.d.ts +1 -1
- package/dist/commands/cardAdd.d.ts.map +1 -1
- package/dist/commands/cardAdd.js +16 -6
- package/dist/commands/cardAdd.js.map +1 -1
- package/dist/commands/cardDashboard.js +1 -1
- package/dist/commands/cardDashboard.js.map +1 -1
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -314
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hookCommand.d.ts.map +1 -1
- package/dist/commands/hookCommand.js +6 -7
- package/dist/commands/hookCommand.js.map +1 -1
- package/dist/commands/pmCommand.js +1 -1
- package/dist/commands/pmCommand.js.map +1 -1
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +60 -37
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +3 -30
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/commands/tick.js +1 -1
- package/dist/commands/tick.js.map +1 -1
- package/dist/core/checklist.d.ts +22 -0
- package/dist/core/checklist.d.ts.map +1 -0
- package/dist/core/checklist.js +38 -0
- package/dist/core/checklist.js.map +1 -0
- package/dist/core/checklist.test.d.ts +2 -0
- package/dist/core/checklist.test.d.ts.map +1 -0
- package/dist/core/checklist.test.js +74 -0
- package/dist/core/checklist.test.js.map +1 -0
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/config.test.js +7 -4
- package/dist/core/config.test.js.map +1 -1
- package/dist/core/context.d.ts +1 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +197 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +190 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/engines/EventHandler.test.js +3 -3
- package/dist/engines/EventHandler.test.js.map +1 -1
- package/dist/engines/MonitorEngine.js +2 -2
- package/dist/engines/MonitorEngine.js.map +1 -1
- package/dist/engines/SchedulerEngine.js +1 -1
- package/dist/engines/SchedulerEngine.js.map +1 -1
- package/dist/engines/StageEngine.js +3 -3
- package/dist/engines/StageEngine.js.map +1 -1
- package/dist/engines/engine-pipeline-adapter.test.js +2 -2
- package/dist/engines/engine-pipeline-adapter.test.js.map +1 -1
- package/dist/interfaces/TaskBackend.d.ts +3 -1
- package/dist/interfaces/TaskBackend.d.ts.map +1 -1
- package/dist/main.js +19 -17
- package/dist/main.js.map +1 -1
- package/dist/models/types.d.ts +16 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.d.ts +2 -1
- package/dist/providers/MarkdownTaskBackend.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.js +28 -5
- package/dist/providers/MarkdownTaskBackend.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +5 -7
- package/dist/providers/registry.js.map +1 -1
- package/package.json +1 -1
- package/project-template/.claude/hooks/start.sh +44 -0
- package/project-template/.claude/settings.json +1 -1
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- 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.
|