@booklib/skills 1.2.0 → 1.3.1

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 (100) hide show
  1. package/CONTRIBUTING.md +122 -0
  2. package/README.md +20 -2
  3. package/ROADMAP.md +36 -0
  4. package/animation-at-work/evals/evals.json +44 -0
  5. package/animation-at-work/examples/after.md +64 -0
  6. package/animation-at-work/examples/before.md +35 -0
  7. package/animation-at-work/scripts/audit_animations.py +295 -0
  8. package/bin/skills.js +552 -42
  9. package/clean-code-reviewer/SKILL.md +109 -1
  10. package/clean-code-reviewer/evals/evals.json +121 -3
  11. package/clean-code-reviewer/examples/after.md +48 -0
  12. package/clean-code-reviewer/examples/before.md +33 -0
  13. package/clean-code-reviewer/references/api_reference.md +158 -0
  14. package/clean-code-reviewer/references/practices-catalog.md +282 -0
  15. package/clean-code-reviewer/references/review-checklist.md +254 -0
  16. package/clean-code-reviewer/scripts/pre-review.py +206 -0
  17. package/data-intensive-patterns/evals/evals.json +43 -0
  18. package/data-intensive-patterns/examples/after.md +61 -0
  19. package/data-intensive-patterns/examples/before.md +38 -0
  20. package/data-intensive-patterns/scripts/adr.py +213 -0
  21. package/data-pipelines/evals/evals.json +45 -0
  22. package/data-pipelines/examples/after.md +97 -0
  23. package/data-pipelines/examples/before.md +37 -0
  24. package/data-pipelines/scripts/new_pipeline.py +444 -0
  25. package/design-patterns/evals/evals.json +46 -0
  26. package/design-patterns/examples/after.md +52 -0
  27. package/design-patterns/examples/before.md +29 -0
  28. package/design-patterns/scripts/scaffold.py +807 -0
  29. package/domain-driven-design/SKILL.md +120 -0
  30. package/domain-driven-design/evals/evals.json +48 -0
  31. package/domain-driven-design/examples/after.md +80 -0
  32. package/domain-driven-design/examples/before.md +43 -0
  33. package/domain-driven-design/scripts/scaffold.py +421 -0
  34. package/effective-java/evals/evals.json +46 -0
  35. package/effective-java/examples/after.md +83 -0
  36. package/effective-java/examples/before.md +37 -0
  37. package/effective-java/scripts/checkstyle_setup.py +211 -0
  38. package/effective-kotlin/evals/evals.json +45 -0
  39. package/effective-kotlin/examples/after.md +36 -0
  40. package/effective-kotlin/examples/before.md +38 -0
  41. package/effective-python/evals/evals.json +44 -0
  42. package/effective-python/examples/after.md +56 -0
  43. package/effective-python/examples/before.md +40 -0
  44. package/effective-python/references/api_reference.md +218 -0
  45. package/effective-python/references/practices-catalog.md +483 -0
  46. package/effective-python/references/review-checklist.md +190 -0
  47. package/effective-python/scripts/lint.py +173 -0
  48. package/kotlin-in-action/evals/evals.json +43 -0
  49. package/kotlin-in-action/examples/after.md +53 -0
  50. package/kotlin-in-action/examples/before.md +39 -0
  51. package/kotlin-in-action/scripts/setup_detekt.py +224 -0
  52. package/lean-startup/evals/evals.json +43 -0
  53. package/lean-startup/examples/after.md +80 -0
  54. package/lean-startup/examples/before.md +34 -0
  55. package/lean-startup/scripts/new_experiment.py +286 -0
  56. package/microservices-patterns/SKILL.md +140 -0
  57. package/microservices-patterns/evals/evals.json +45 -0
  58. package/microservices-patterns/examples/after.md +69 -0
  59. package/microservices-patterns/examples/before.md +40 -0
  60. package/microservices-patterns/scripts/new_service.py +583 -0
  61. package/package.json +2 -8
  62. package/refactoring-ui/evals/evals.json +45 -0
  63. package/refactoring-ui/examples/after.md +85 -0
  64. package/refactoring-ui/examples/before.md +58 -0
  65. package/refactoring-ui/scripts/audit_css.py +250 -0
  66. package/skill-router/SKILL.md +142 -0
  67. package/skill-router/evals/evals.json +38 -0
  68. package/skill-router/examples/after.md +63 -0
  69. package/skill-router/examples/before.md +39 -0
  70. package/skill-router/references/api_reference.md +24 -0
  71. package/skill-router/references/routing-heuristics.md +89 -0
  72. package/skill-router/references/skill-catalog.md +156 -0
  73. package/skill-router/scripts/route.py +266 -0
  74. package/storytelling-with-data/evals/evals.json +47 -0
  75. package/storytelling-with-data/examples/after.md +50 -0
  76. package/storytelling-with-data/examples/before.md +33 -0
  77. package/storytelling-with-data/scripts/chart_review.py +301 -0
  78. package/system-design-interview/evals/evals.json +45 -0
  79. package/system-design-interview/examples/after.md +94 -0
  80. package/system-design-interview/examples/before.md +27 -0
  81. package/system-design-interview/scripts/new_design.py +421 -0
  82. package/using-asyncio-python/evals/evals.json +43 -0
  83. package/using-asyncio-python/examples/after.md +68 -0
  84. package/using-asyncio-python/examples/before.md +39 -0
  85. package/using-asyncio-python/scripts/check_blocking.py +270 -0
  86. package/web-scraping-python/evals/evals.json +46 -0
  87. package/web-scraping-python/examples/after.md +109 -0
  88. package/web-scraping-python/examples/before.md +40 -0
  89. package/web-scraping-python/scripts/new_scraper.py +231 -0
  90. /package/{effective-python-skill → effective-python}/SKILL.md +0 -0
  91. /package/{effective-python-skill → effective-python}/ref-01-pythonic-thinking.md +0 -0
  92. /package/{effective-python-skill → effective-python}/ref-02-lists-and-dicts.md +0 -0
  93. /package/{effective-python-skill → effective-python}/ref-03-functions.md +0 -0
  94. /package/{effective-python-skill → effective-python}/ref-04-comprehensions-generators.md +0 -0
  95. /package/{effective-python-skill → effective-python}/ref-05-classes-interfaces.md +0 -0
  96. /package/{effective-python-skill → effective-python}/ref-06-metaclasses-attributes.md +0 -0
  97. /package/{effective-python-skill → effective-python}/ref-07-concurrency.md +0 -0
  98. /package/{effective-python-skill → effective-python}/ref-08-robustness-performance.md +0 -0
  99. /package/{effective-python-skill → effective-python}/ref-09-testing-debugging.md +0 -0
  100. /package/{effective-python-skill → effective-python}/ref-10-collaboration.md +0 -0
@@ -0,0 +1,483 @@
1
+ # Effective Python — Practices Catalog
2
+
3
+ The 20 most impactful items from Brett Slatkin's *Effective Python* (2nd Edition),
4
+ each with the problem it solves, the Pythonic solution, and a before/after code example.
5
+
6
+ ---
7
+
8
+ ## Item 4 — Prefer Interpolated F-Strings Over C-style Format Strings and str.format
9
+
10
+ **Problem:** `%` format strings are verbose and error-prone with multiple values. `.format()` is better but still cluttered with braces and positional indices. Both separate the format template from the values being formatted.
11
+
12
+ **Solution:** Use f-strings — they embed expressions directly in string literals, are more readable, and support the full power of format specifiers inline.
13
+
14
+ ```python
15
+ # Before
16
+ name, age = "Alice", 30
17
+ msg = "User %s is %d years old" % (name, age)
18
+ msg = "User {} is {} years old".format(name, age)
19
+
20
+ # After
21
+ msg = f"User {name} is {age} years old"
22
+ msg = f"Pi to 4 places: {3.14159:.4f}"
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Item 6 — Prefer Multiple Assignment Unpacking Over Indexing
28
+
29
+ **Problem:** Accessing sequence elements by index (`pair[0]`, `pair[1]`) is opaque — the reader must know what position 0 or 1 represents. It is verbose and error-prone when indices change.
30
+
31
+ **Solution:** Unpack sequences into named variables in a single assignment. The names document meaning; the structure enforces the expected shape.
32
+
33
+ ```python
34
+ # Before
35
+ item = ("Alice", 25, "engineer")
36
+ name = item[0]
37
+ age = item[1]
38
+ role = item[2]
39
+
40
+ # After
41
+ name, age, role = item
42
+ first, *rest = items # catch-all unpacking
43
+ head, *middle, tail = items # discard the middle
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Item 7 — Prefer enumerate Over range for Indexed Iteration
49
+
50
+ **Problem:** `for i in range(len(sequence))` is verbose and indirect. It requires manually indexing the sequence on each iteration, which is error-prone and harder to read.
51
+
52
+ **Solution:** Use `enumerate(sequence)` to get both the index and value together. Pass a `start` argument to control the counter's starting value.
53
+
54
+ ```python
55
+ # Before
56
+ flavors = ["vanilla", "chocolate", "strawberry"]
57
+ for i in range(len(flavors)):
58
+ print(f"{i}: {flavors[i]}")
59
+
60
+ # After
61
+ for i, flavor in enumerate(flavors, start=1):
62
+ print(f"{i}: {flavor}")
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Item 8 — Use zip to Process Iterators in Parallel
68
+
69
+ **Problem:** Iterating over two related sequences using a shared index is verbose and fragile. If the sequences have different lengths, off-by-one bugs or IndexError occur silently.
70
+
71
+ **Solution:** Use `zip()` to pair up items from multiple sequences. Use `itertools.zip_longest()` when sequences may have different lengths and you want to handle all items.
72
+
73
+ ```python
74
+ # Before
75
+ for i in range(len(names)):
76
+ print(f"{names[i]}: {scores[i]}")
77
+
78
+ # After
79
+ from itertools import zip_longest
80
+ for name, score in zip(names, scores):
81
+ print(f"{name}: {score}")
82
+ for name, score in zip_longest(names, scores, fillvalue=0):
83
+ print(f"{name}: {score}")
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Item 20 — Prefer Raising Exceptions to Returning None
89
+
90
+ **Problem:** Functions that return `None` to signal failure require callers to check the return value — and if they forget, the `None` silently propagates until it crashes somewhere unrelated. `None` and `0`, `""`, `[]` all evaluate as falsy, making the check ambiguous.
91
+
92
+ **Solution:** Raise an exception for failure cases. Callers that want to handle the failure use `try/except`; callers that don't care let the exception propagate. The failure is never silently ignored.
93
+
94
+ ```python
95
+ # Before
96
+ def parse_ratio(a, b):
97
+ try:
98
+ return a / b
99
+ except ZeroDivisionError:
100
+ return None # caller may forget to check
101
+
102
+ result = parse_ratio(5, 0)
103
+ if result: # BUG: 0.0 is also falsy!
104
+ print(result)
105
+
106
+ # After
107
+ def parse_ratio(a, b):
108
+ try:
109
+ return a / b
110
+ except ZeroDivisionError:
111
+ raise ValueError(f"parse_ratio({a}, {b}): b must be non-zero")
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Item 24 — Use None and Docstrings to Specify Dynamic Default Arguments
117
+
118
+ **Problem:** Using a mutable object (list, dict) or dynamic expression (`datetime.now()`) as a default argument value is evaluated once at function definition time, not at call time. All callers share the same default object, leading to subtle, hard-to-find bugs.
119
+
120
+ **Solution:** Use `None` as the default and initialize the mutable/dynamic value inside the function body. Document the actual default in the docstring.
121
+
122
+ ```python
123
+ # Before — BUG: all calls share the same list
124
+ def log(message, when=datetime.now(), tags=[]):
125
+ tags.append("debug") # mutates the shared default!
126
+ print(f"{when}: {message} {tags}")
127
+
128
+ # After
129
+ def log(message, when=None, tags=None):
130
+ """Log a message with a timestamp.
131
+
132
+ Args:
133
+ when: Timestamp for the log entry. Defaults to now.
134
+ tags: List of tags. Defaults to an empty list.
135
+ """
136
+ if when is None:
137
+ when = datetime.now()
138
+ if tags is None:
139
+ tags = []
140
+ tags.append("debug")
141
+ print(f"{when}: {message} {tags}")
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Item 25 — Enforce Clarity with Keyword-Only and Positional-Only Arguments
147
+
148
+ **Problem:** Functions with multiple boolean or configuration parameters are ambiguous at the call site. `safe_divide(1.0, 5.0, True, False)` — what do `True` and `False` mean? Callers can pass args in the wrong order and get no error.
149
+
150
+ **Solution:** Use keyword-only arguments (after `*` or `*args`) to force callers to name certain parameters. Use positional-only arguments (before `/`) to prevent callers from naming implementation details.
151
+
152
+ ```python
153
+ # Before — ambiguous call
154
+ def safe_divide(number, divisor, ignore_overflow, ignore_zero):
155
+ ...
156
+ safe_divide(1.0, 5.0, True, False) # what does True mean?
157
+
158
+ # After — keyword-only enforces clarity
159
+ def safe_divide(number, divisor, *, ignore_overflow=False, ignore_zero=False):
160
+ ...
161
+ safe_divide(1.0, 5.0, ignore_zero=True) # clear at the call site
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Item 26 — Define Function Decorators with functools.wraps
167
+
168
+ **Problem:** A decorator replaces the wrapped function with a new wrapper function. Without `functools.wraps`, the wrapped function loses its `__name__`, `__doc__`, `__module__`, and other metadata. This breaks debugging, documentation tools, and introspection.
169
+
170
+ **Solution:** Apply `@functools.wraps(func)` to the inner wrapper function in every decorator.
171
+
172
+ ```python
173
+ # Before — metadata lost
174
+ def trace(func):
175
+ def wrapper(*args, **kwargs):
176
+ result = func(*args, **kwargs)
177
+ return result
178
+ return wrapper # wrapper.__name__ is "wrapper", not "fibonacci"
179
+
180
+ # After — metadata preserved
181
+ import functools
182
+ def trace(func):
183
+ @functools.wraps(func)
184
+ def wrapper(*args, **kwargs):
185
+ result = func(*args, **kwargs)
186
+ return result
187
+ return wrapper # wrapper.__name__ is "fibonacci"
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Item 27 — Use Comprehensions Instead of map and filter
193
+
194
+ **Problem:** `map()` and `filter()` with `lambda` are verbose and require the reader to mentally parse the lambda, the outer function, and the argument order. They return iterators that must be explicitly materialized.
195
+
196
+ **Solution:** Use list, dict, and set comprehensions. They are more readable, support conditions inline, and work naturally for all collection types.
197
+
198
+ ```python
199
+ # Before
200
+ squares_of_evens = list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, range(10))))
201
+
202
+ # After
203
+ squares_of_evens = [x**2 for x in range(10) if x % 2 == 0]
204
+
205
+ # Dict comprehension
206
+ square_map = {x: x**2 for x in range(10)}
207
+
208
+ # Set comprehension
209
+ unique_lengths = {len(name) for name in names}
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Item 30 — Consider Generators Instead of Returning Lists
215
+
216
+ **Problem:** A function that builds and returns a list must hold the entire result in memory before the first item is available to the caller. For large sequences, this wastes memory and delays the first result.
217
+
218
+ **Solution:** Use `yield` to make the function a generator. The caller receives items one at a time; no full list is ever built in memory. The generator is lazy — it computes only what the caller consumes.
219
+
220
+ ```python
221
+ # Before — entire list built in memory
222
+ def index_words(text):
223
+ result = []
224
+ for i, char in enumerate(text):
225
+ if i == 0 or char == " ":
226
+ result.append(i)
227
+ return result
228
+
229
+ # After — generator, one item at a time
230
+ def index_words(text):
231
+ for i, char in enumerate(text):
232
+ if i == 0 or char == " ":
233
+ yield i
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Item 37 — Compose Classes Instead of Nesting Many Levels of Built-in Types
239
+
240
+ **Problem:** Deeply nested dicts, lists, and tuples (e.g., a dict of dicts of lists of tuples) are hard to read, document, and extend. Adding a new field requires updating every place that constructs or unpacks the data.
241
+
242
+ **Solution:** When a structure exceeds two levels of nesting, define small helper classes or use `collections.namedtuple` / `dataclasses.dataclass`. This provides named access, documentation, and the ability to add methods later.
243
+
244
+ ```python
245
+ # Before — opaque nested dict
246
+ grades = {"Alice": {"Math": [90, 88], "English": [75, 82]}}
247
+
248
+ # After — self-documenting classes
249
+ from dataclasses import dataclass, field
250
+ @dataclass
251
+ class Subject:
252
+ name: str
253
+ scores: list = field(default_factory=list)
254
+ def average(self): return sum(self.scores) / len(self.scores)
255
+
256
+ @dataclass
257
+ class Student:
258
+ name: str
259
+ subjects: dict = field(default_factory=dict)
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Item 40 — Initialize Parent Classes with super()
265
+
266
+ **Problem:** Calling `ParentClass.__init__(self)` directly breaks with multiple inheritance and the Method Resolution Order (MRO). It can call a parent's `__init__` twice or miss it entirely when the class hierarchy is diamond-shaped.
267
+
268
+ **Solution:** Always use `super().__init__()` without arguments. Python resolves the correct class to call based on the MRO, making multiple inheritance predictable.
269
+
270
+ ```python
271
+ # Before — fragile with multiple inheritance
272
+ class MyChild(ParentA, ParentB):
273
+ def __init__(self):
274
+ ParentA.__init__(self) # may call Base twice in diamond inheritance
275
+ ParentB.__init__(self)
276
+
277
+ # After — MRO-correct
278
+ class MyChild(ParentA, ParentB):
279
+ def __init__(self):
280
+ super().__init__() # follows MRO, each parent called exactly once
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Item 44 — Use Plain Attributes Instead of Get and Set Methods
286
+
287
+ **Problem:** Java-style getter/setter method pairs (`get_voltage()`, `set_voltage()`) are verbose and un-Pythonic. They add noise without benefit when no special behavior is needed.
288
+
289
+ **Solution:** Use plain public attributes by default. If you later need validation, caching, or computed values, migrate to `@property` without changing the external interface.
290
+
291
+ ```python
292
+ # Before — Java-style
293
+ class OldResistor:
294
+ def __init__(self, ohms):
295
+ self._ohms = ohms
296
+ def get_ohms(self): return self._ohms
297
+ def set_ohms(self, ohms): self._ohms = ohms
298
+
299
+ # After — plain attribute first
300
+ class Resistor:
301
+ def __init__(self, ohms):
302
+ self.ohms = ohms # plain attribute; add @property later if needed
303
+ self.voltage = 0
304
+ self.current = 0
305
+ ```
306
+
307
+ ---
308
+
309
+ ## Item 45 — Consider @property Instead of Refactoring Attributes
310
+
311
+ **Problem:** Once a class is published with a plain attribute, changing it to a method breaks all callers. If behavior (validation, computation, side effects) is needed on attribute access, the API must change.
312
+
313
+ **Solution:** Use `@property` to add behavior to attribute access while keeping the same attribute-style interface. The caller never knows whether it's a stored value or a computed one.
314
+
315
+ ```python
316
+ # Before — breaks callers if changed to a method
317
+ class Circle:
318
+ def __init__(self, radius):
319
+ self.radius = radius
320
+ def area(self): # caller must use circle.area()
321
+ return 3.14 * self.radius ** 2
322
+
323
+ # After — area looks like an attribute
324
+ class Circle:
325
+ def __init__(self, radius):
326
+ self.radius = radius
327
+ @property
328
+ def area(self): # caller uses circle.area (no parentheses)
329
+ return 3.14 * self.radius ** 2
330
+ ```
331
+
332
+ ---
333
+
334
+ ## Item 53 — Use Threads for Blocking I/O, Avoid for Parallelism
335
+
336
+ **Problem:** Python's Global Interpreter Lock (GIL) prevents multiple threads from executing Python bytecode simultaneously. Using threads to parallelize CPU-bound work achieves no speedup and adds complexity.
337
+
338
+ **Solution:** Use threads only for blocking I/O (network calls, disk reads) where threads wait idle most of the time. Use `multiprocessing` or `concurrent.futures.ProcessPoolExecutor` for CPU-bound parallelism.
339
+
340
+ ```python
341
+ # CPU-bound — threads give no benefit (GIL)
342
+ from concurrent.futures import ProcessPoolExecutor
343
+ def factorize(number): ... # CPU work
344
+ with ProcessPoolExecutor() as pool:
345
+ results = list(pool.map(factorize, numbers))
346
+
347
+ # I/O-bound — threads work well
348
+ from concurrent.futures import ThreadPoolExecutor
349
+ def fetch_url(url): ... # blocking network I/O
350
+ with ThreadPoolExecutor(max_workers=10) as pool:
351
+ results = list(pool.map(fetch_url, urls))
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Item 54 — Use Lock to Prevent Data Races in Threads
357
+
358
+ **Problem:** When multiple threads read and write shared mutable state without synchronization, the operations interleave unpredictably. The GIL does not protect against data races at the level of compound operations like `counter += 1`.
359
+
360
+ **Solution:** Use `threading.Lock` to protect all accesses to shared mutable state. Acquire the lock before reading or writing, release it after. Use `with lock:` for automatic release even on exceptions.
361
+
362
+ ```python
363
+ # Before — data race: final count is unpredictable
364
+ counter = 0
365
+ def increment():
366
+ global counter
367
+ counter += 1 # not atomic: read, add, write — can interleave
368
+
369
+ # After — Lock prevents interleaving
370
+ import threading
371
+ lock = threading.Lock()
372
+ counter = 0
373
+ def increment():
374
+ global counter
375
+ with lock:
376
+ counter += 1
377
+ ```
378
+
379
+ ---
380
+
381
+ ## Item 65 — Take Advantage of Each Block in try/except/else/finally
382
+
383
+ **Problem:** Developers often use `try/except` with all code inside `try`, catching exceptions that were not intended to be caught. The `else` and `finally` clauses are underused.
384
+
385
+ **Solution:** Use the full four-part structure: `try` for only the risky operation, `except` for specific exceptions, `else` for code that runs only on success, `finally` for cleanup that always runs.
386
+
387
+ ```python
388
+ # Before — too broad, may catch unexpected exceptions
389
+ try:
390
+ data = json.loads(text)
391
+ process(data) # this exception is caught too!
392
+ save(data)
393
+ except ValueError:
394
+ log("Invalid JSON")
395
+
396
+ # After — precise structure
397
+ try:
398
+ data = json.loads(text) # only the risky part
399
+ except ValueError as e:
400
+ log(f"Invalid JSON: {e}")
401
+ else:
402
+ process(data) # runs only if no exception
403
+ save(data)
404
+ finally:
405
+ cleanup() # always runs
406
+ ```
407
+
408
+ ---
409
+
410
+ ## Item 78 — Use TestCase Subclasses for Tests
411
+
412
+ **Problem:** Writing tests as standalone functions without a `TestCase` class misses built-in helpers: `setUp`/`tearDown` for fixtures, `subTest` for variations, and assertion methods like `assertRaises`, `assertEqual`, `assertAlmostEqual` that produce clear failure messages.
413
+
414
+ **Solution:** Subclass `unittest.TestCase`. Use `setUp` for common setup and `tearDown` for cleanup. Use `subTest` to run the same test with multiple inputs.
415
+
416
+ ```python
417
+ # After — structured TestCase
418
+ import unittest
419
+ class TestMyFunction(unittest.TestCase):
420
+ def setUp(self):
421
+ self.db = FakeDatabase()
422
+ def tearDown(self):
423
+ self.db.close()
424
+ def test_normal_case(self):
425
+ self.assertEqual(my_function(self.db, 1), "expected")
426
+ def test_multiple_inputs(self):
427
+ cases = [(1, "a"), (2, "b"), (3, "c")]
428
+ for inp, expected in cases:
429
+ with self.subTest(inp=inp):
430
+ self.assertEqual(my_function(self.db, inp), expected)
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Item 80 — Consider Interactive Debugging with pdb
436
+
437
+ **Problem:** Adding `print()` statements to debug code produces cluttered output, requires multiple edit-run cycles, and must be cleaned up afterward. It provides no way to interactively explore state.
438
+
439
+ **Solution:** Use `breakpoint()` (Python 3.7+) to drop into the interactive pdb debugger at a specific line. Step through code, inspect variables, call functions, and modify state interactively. Remove the breakpoint when done.
440
+
441
+ ```python
442
+ # Before — print debugging
443
+ def complex_function(data):
444
+ print(f"DEBUG: data={data}") # must be cleaned up
445
+ result = transform(data)
446
+ print(f"DEBUG: result={result}")
447
+ return result
448
+
449
+ # After — interactive debugger
450
+ def complex_function(data):
451
+ breakpoint() # drops into pdb; type 'n' (next), 's' (step), 'p var' (print)
452
+ result = transform(data)
453
+ return result
454
+ ```
455
+
456
+ ---
457
+
458
+ ## Item 84 — Write Docstrings for Every Function, Class, and Module
459
+
460
+ **Problem:** Functions and classes without docstrings force readers to read the implementation to understand the contract. Automated documentation tools (`pydoc`, Sphinx) produce empty or useless output.
461
+
462
+ **Solution:** Write a docstring for every public module, class, and function. Describe what it does, its arguments, return value, and any exceptions it raises. Use the first line as a one-sentence summary.
463
+
464
+ ```python
465
+ # Before — no documentation
466
+ def find_anagrams(word, candidates):
467
+ word_letters = Counter(word.lower())
468
+ return [c for c in candidates if Counter(c.lower()) == word_letters]
469
+
470
+ # After — documented contract
471
+ def find_anagrams(word, candidates):
472
+ """Find all anagrams of word in the candidates list.
473
+
474
+ Args:
475
+ word: The word to find anagrams for. Case-insensitive.
476
+ candidates: Sequence of words to search.
477
+
478
+ Returns:
479
+ List of strings from candidates that are anagrams of word.
480
+ """
481
+ word_letters = Counter(word.lower())
482
+ return [c for c in candidates if Counter(c.lower()) == word_letters]
483
+ ```
@@ -0,0 +1,190 @@
1
+ # Effective Python — Code Review Checklist
2
+
3
+ Systematic checklist for reviewing Python code against the 90 best practices from
4
+ *Effective Python: 90 Specific Ways to Write Better Python* (2nd Edition) by Brett Slatkin.
5
+
6
+ ---
7
+
8
+ ## Chapter 1 — Pythonic Thinking (Items 1–10)
9
+
10
+ ### Style and Idiom
11
+ - [ ] **Item 1 — Python version** — Is the code targeting a current, supported Python version? Are any version-specific compatibility shims present that can be removed?
12
+ - [ ] **Item 2 — PEP 8 style** — Does the code follow PEP 8? snake_case for functions/variables, PascalCase for classes, UPPER_CASE for module-level constants, 4-space indentation, 79-character lines?
13
+ - [ ] **Item 3 — bytes vs. str** — Is the code clear about whether it's operating on `bytes` or `str`? Are encoding/decoding boundaries explicit? Is `open()` called with the correct mode (`'b'` vs. `'t'`)?
14
+
15
+ ### String Formatting
16
+ - [ ] **Item 4 — f-strings** — Are f-strings used instead of `%` formatting or `.format()`? Is every formatted string using the simplest, most readable form available?
17
+
18
+ ### Data Unpacking
19
+ - [ ] **Item 5 — Helper functions over complex expressions** — Are complex multi-step expressions broken into helper functions with descriptive names?
20
+ - [ ] **Item 6 — Unpacking instead of indexing** — Are sequences unpacked directly (`first, second = pair`) instead of accessed by index (`pair[0]`, `pair[1]`)?
21
+ - [ ] **Item 7 — enumerate instead of range** — Is `enumerate(sequence)` used instead of `range(len(sequence))` when both the index and value are needed?
22
+ - [ ] **Item 8 — zip for parallel iteration** — Is `zip()` used to iterate over multiple sequences in parallel instead of indexing? Is `itertools.zip_longest` used when sequence lengths might differ?
23
+
24
+ ### Control Flow
25
+ - [ ] **Item 9 — No else after for/while** — Are `else` blocks after `for` or `while` loops absent? This construct is confusing — use a flag variable or `break` + check instead.
26
+ - [ ] **Item 10 — Walrus operator** — Is `:=` used to reduce redundant expressions where appropriate (e.g., assigning and testing in the same expression in a `while` or `if`)? Is it avoided where it would reduce clarity?
27
+
28
+ ---
29
+
30
+ ## Chapter 2 — Lists and Dicts (Items 11–18)
31
+
32
+ ### Sequence Operations
33
+ - [ ] **Item 11 — Slicing** — Is slicing used idiomatically? Are start/end omitted when slicing from the beginning or to the end (`a[:5]`, `a[3:]`)? Is stride-slicing (`a[::2]`) with start/end avoided in favor of clarity?
34
+ - [ ] **Item 12 — No start, stop, stride together** — Is `a[2::2]` or `a[:-1:2]` avoided in favor of two separate operations? Combining start, stop, and stride in one slice is hard to read.
35
+ - [ ] **Item 13 — Catch-all unpacking** — Is starred unpacking (`first, *rest = items`) used instead of slicing for splitting sequences? Is `*_` used to discard unwanted items cleanly?
36
+
37
+ ### Sorting
38
+ - [ ] **Item 14 — Sort with key parameter** — Are `sort()` and `sorted()` called with a `key=` function rather than `cmp=` or manual tuple sorting hacks?
39
+ - [ ] **Item 15 — Sorting by multiple criteria** — When sorting by multiple fields in different directions, is `key=` with a tuple used, combined with `reverse=True` where needed, or multiple stable sorts applied in reverse priority order?
40
+
41
+ ### Dicts
42
+ - [ ] **Item 16 — dict.get with default** — Is `dict.get(key, default)` used instead of checking `key in dict` before accessing? Are `setdefault` or `defaultdict` used for dict initialization patterns?
43
+ - [ ] **Item 17 — defaultdict for internal state** — Is `collections.defaultdict` used for dicts where missing keys should be initialized automatically? Is `__missing__` used for more complex initialization logic?
44
+ - [ ] **Item 18 — __missing__ for custom defaults** — When `defaultdict` is insufficient (e.g., default value depends on the key), is `__missing__` implemented on a `dict` subclass?
45
+
46
+ ---
47
+
48
+ ## Chapter 3 — Functions (Items 19–26)
49
+
50
+ ### Return Values and Errors
51
+ - [ ] **Item 19 — Never unpack more than 3 variables** — Does any function return a tuple of more than 3 elements for unpacking? Use a `namedtuple` or small class instead.
52
+ - [ ] **Item 20 — Raise exceptions instead of returning None** — Do functions raise exceptions for failure cases instead of returning `None`? Is `None` used only to mean "no value" — not "error"?
53
+
54
+ ### Closures and Scoping
55
+ - [ ] **Item 21 — Closures and variable scope** — Do closures reference variables from the enclosing scope correctly? Is `nonlocal` used when a closure must modify an enclosing variable?
56
+ - [ ] **Item 22 — Variable positional arguments with *args** — When a function accepts a variable number of positional args, is `*args` used? Is the caller passing generators directly into `*args` functions (risky — exhausts the generator)?
57
+
58
+ ### Function Arguments
59
+ - [ ] **Item 23 — Keyword arguments** — Are functions that accept many arguments using keyword arguments for clarity at the call site?
60
+ - [ ] **Item 24 — None for dynamic defaults** — Are mutable objects or dynamic values (lists, dicts, `datetime.now()`) never used as default argument values? Is `None` used as the default with a `if param is None: param = []` pattern?
61
+ - [ ] **Item 25 — Keyword-only and positional-only arguments** — Are ambiguous boolean or configuration arguments declared keyword-only (after `*`)? Is the public/private boundary of an API enforced with positional-only args (before `/`)?
62
+
63
+ ### Decorators
64
+ - [ ] **Item 26 — functools.wraps on all decorators** — Does every decorator use `@functools.wraps(func)` to preserve the wrapped function's `__name__`, `__doc__`, and other metadata?
65
+
66
+ ---
67
+
68
+ ## Chapter 4 — Comprehensions and Generators (Items 27–36)
69
+
70
+ ### Comprehensions
71
+ - [ ] **Item 27 — Comprehensions over map/filter** — Are list/dict/set comprehensions used instead of `map()` and `filter()` with `lambda`? Comprehensions are more readable.
72
+ - [ ] **Item 28 — No more than two expressions** — Do all comprehensions contain at most two expressions (e.g., one loop and one condition)? Comprehensions with three or more expressions are harder to read than a plain loop.
73
+ - [ ] **Item 29 — No walrus in comprehensions for leaking** — Is `:=` in comprehensions used carefully? Walrus operator assignments in comprehensions can leak scope in unexpected ways.
74
+ - [ ] **Item 30 — Generators instead of lists** — When a function produces a sequence for a caller to iterate, does it use `yield` instead of building and returning a list? Does it avoid holding the entire sequence in memory?
75
+
76
+ ### Generators and Iterators
77
+ - [ ] **Item 31 — Aware of single-iteration behavior** — Does code that iterates over an iterator do so only once? Is a container (list, tuple) used when multiple iterations are needed?
78
+ - [ ] **Item 32 — Iterator protocol** — When implementing a custom iterable, does the class implement `__iter__` returning `self` and `__next__`? Or does `__iter__` return a generator?
79
+ - [ ] **Item 33 — yield from for delegation** — Does generator code that delegates to another iterator use `yield from` instead of a manual `for` loop with `yield`?
80
+ - [ ] **Item 34 — send for generator communication** — If a generator must receive values from the caller, is `.send()` used? Is the protocol clear and documented?
81
+ - [ ] **Item 35 — throw and close on generators** — Are `generator.throw()` and `generator.close()` used for error injection and cleanup when needed?
82
+ - [ ] **Item 36 — itertools for complex iteration** — Are `itertools` functions (`chain`, `islice`, `tee`, `zip_longest`, `product`, `permutations`, `combinations`, `groupby`) used instead of hand-rolled equivalents?
83
+
84
+ ---
85
+
86
+ ## Chapter 5 — Classes and Interfaces (Items 37–43)
87
+
88
+ ### Class Design
89
+ - [ ] **Item 37 — Compose classes instead of deep nesting** — Are deeply nested dicts/lists/tuples replaced with small helper classes or `namedtuple`/`dataclass`? Is composition preferred over inheritance?
90
+ - [ ] **Item 38 — Simple interfaces with functions** — When a simple callback interface is needed, is a plain function (or callable object) accepted instead of defining a single-method interface class?
91
+ - [ ] **Item 39 — @classmethod for polymorphic construction** — Are alternative constructors implemented as `@classmethod` methods rather than freestanding functions or `__init__` overloads?
92
+ - [ ] **Item 40 — super().__init__() always called** — Does every class that inherits from another call `super().__init__()` in its own `__init__`? Is `super()` used without arguments (Python 3 style)?
93
+
94
+ ### Mix-ins and Attributes
95
+ - [ ] **Item 41 — Mix-ins for reusable behavior** — Are mix-ins used for reusable behavior that should be composed into multiple classes? Do mix-ins avoid `__init__` and instance state?
96
+ - [ ] **Item 42 — Public attributes over private** — Are attributes defined as public (`self.value`) by default? Is `__private` (name-mangled) used only when deliberate subclass protection is needed? Is `_protected` used to signal "internal use" conventionally?
97
+ - [ ] **Item 43 — Inherit from collections.abc** — When defining a custom container type (sequence, mapping, set), does the class inherit from the appropriate `collections.abc` abstract base class?
98
+
99
+ ---
100
+
101
+ ## Chapter 6 — Metaclasses and Attributes (Items 44–51)
102
+
103
+ ### Properties and Descriptors
104
+ - [ ] **Item 44 — Plain attributes over getter/setter** — Are plain public attributes used instead of getter/setter method pairs? Java-style `get_value()`/`set_value()` methods are un-Pythonic.
105
+ - [ ] **Item 45 — @property for special behavior** — When an attribute access needs validation, lazy computation, or side effects, is `@property` used? Does the getter avoid slow computation or surprising side effects?
106
+ - [ ] **Item 46 — @property.setter validates** — Do property setters validate input before assigning? Do setters raise `ValueError` for invalid input rather than silently accepting bad values?
107
+ - [ ] **Item 47 — Descriptors for reusable @property logic** — When the same `@property` logic must apply to multiple attributes or multiple classes, is a descriptor class used instead of repeating the property code?
108
+
109
+ ### Metaclasses and Dynamic Attributes
110
+ - [ ] **Item 48 — __getattr__, __getattribute__, __setattr__** — Is `__getattr__` used for lazy attribute initialization (called only when attribute is missing)? Is `__getattribute__` avoided unless interception of all attribute access is genuinely needed?
111
+ - [ ] **Item 49 — __init_subclass__ for subclass validation** — When a base class needs to validate or register all its subclasses at class definition time, is `__init_subclass__` used instead of a metaclass?
112
+ - [ ] **Item 50 — __set_name__ on descriptors** — Do descriptors use `__set_name__` to learn the attribute name they are assigned to, rather than requiring the name to be passed explicitly?
113
+ - [ ] **Item 51 — Class decorators over metaclasses** — When class-level transformation is needed, is a class decorator used rather than a metaclass? Metaclasses should be a last resort.
114
+
115
+ ---
116
+
117
+ ## Chapter 7 — Concurrency and Parallelism (Items 52–64)
118
+
119
+ ### Processes and Threads
120
+ - [ ] **Item 52 — subprocess for child processes** — Are child processes managed through `subprocess.run()` or `Popen` rather than `os.system()` or `os.popen()`?
121
+ - [ ] **Item 53 — Threads only for I/O** — Is the `threading` module used only for blocking I/O concurrency, never for CPU parallelism? Is `multiprocessing` or `concurrent.futures.ProcessPoolExecutor` used for CPU-bound work?
122
+ - [ ] **Item 54 — Lock for thread safety** — Is `threading.Lock` (or `RLock`) used to protect all shared mutable state accessed by multiple threads?
123
+ - [ ] **Item 55 — Queue for thread coordination** — Is `queue.Queue` used for passing work and results between producer and consumer threads instead of shared mutable lists?
124
+
125
+ ### Coroutines and asyncio
126
+ - [ ] **Item 60 — Coroutines for high-concurrency I/O** — Is `asyncio` used for highly concurrent I/O instead of threads when many simultaneous connections are needed?
127
+ - [ ] **Item 61 — Blocking I/O never in async** — Does all `async` code use `await` for I/O? Are blocking calls (`requests.get`, `time.sleep`, file reads without `aiofiles`) absent from `async def` functions?
128
+ - [ ] **Item 62 — asyncio.run at the entry point** — Is `asyncio.run()` used to start the event loop rather than `loop.run_until_complete()`?
129
+ - [ ] **Item 63 — asyncio.gather for concurrent tasks** — When multiple coroutines should run concurrently, is `asyncio.gather()` or `asyncio.create_task()` used instead of awaiting them sequentially?
130
+
131
+ ---
132
+
133
+ ## Chapter 8 — Robustness and Performance (Items 65–79)
134
+
135
+ ### Error Handling
136
+ - [ ] **Item 65 — try/except/else/finally structure** — Does error handling use the full `try/except/else/finally` structure correctly? Is `else` used for code that runs only when no exception is raised? Is `finally` used for cleanup?
137
+ - [ ] **Item 66 — Reraise with raise** — When catching and reraising exceptions, is bare `raise` used to preserve the original traceback, rather than `raise e` which replaces it?
138
+ - [ ] **Item 67 — Exception chaining** — When raising a new exception in response to a caught one, is `raise NewException(...) from original` used to preserve the chain?
139
+ - [ ] **Item 73 — datetime for timezone handling** — Is `datetime` (with `pytz` or `zoneinfo`) used for timezone-aware operations instead of the `time` module?
140
+
141
+ ### Data Structures and Performance
142
+ - [ ] **Item 70 — Profile before optimizing** — Is any performance optimization justified by profiling output (`cProfile`, `timeit`) rather than intuition?
143
+ - [ ] **Item 71 — Prefer deque for FIFO** — Is `collections.deque` used for queues needing efficient append and pop from both ends, rather than a `list` (which makes `list.pop(0)` O(n))?
144
+ - [ ] **Item 72 — Consider bisect for sorted sequences** — Is `bisect.bisect_left` / `bisect.insort` used for efficient insertion and search in sorted sequences rather than a linear scan?
145
+ - [ ] **Item 73 — heapq for priority queues** — Is `heapq` used for priority queue operations rather than re-sorting a list on every insertion?
146
+ - [ ] **Item 74 — Avoid copying on each iteration** — Are large data copies inside loops avoided? Is `memoryview` used for zero-copy slicing of bytes objects?
147
+
148
+ ---
149
+
150
+ ## Chapter 9 — Testing and Debugging (Items 80–85)
151
+
152
+ ### Testing
153
+ - [ ] **Item 78 — TestCase and subTest** — Are tests structured as `unittest.TestCase` subclasses with `setUp`/`tearDown`? Is `self.subTest()` used to run variations within a single test method?
154
+ - [ ] **Item 79 — Mocks for complex dependencies** — Are external dependencies (network, filesystem, databases, time) mocked using `unittest.mock.Mock` or `MagicMock` in tests?
155
+ - [ ] **Item 80 — Encapsulate dependencies** — Is code structured so dependencies can be injected (passed as arguments or set as attributes) rather than hardcoded? Does testability drive class design?
156
+
157
+ ### Debugging
158
+ - [ ] **Item 81 — pdb for debugging** — Is `breakpoint()` (Python 3.7+) used for interactive debugging rather than scatter-gunning print statements?
159
+ - [ ] **Item 82 — tracemalloc for memory** — Is `tracemalloc` used to identify the source of unexpected memory growth rather than guessing?
160
+
161
+ ---
162
+
163
+ ## Chapter 10 — Collaboration (Items 86–90)
164
+
165
+ - [ ] **Item 84 — Docstrings for all public APIs** — Do all public modules, classes, and functions have docstrings? Do docstrings describe *what* the function does, its arguments, return value, and any exceptions raised?
166
+ - [ ] **Item 85 — Packages for API namespacing** — Is code organized into packages with `__init__.py` files? Does the package's `__init__.py` expose a clean public API?
167
+ - [ ] **Item 86 — Root exception for packages** — Does each package define its own root exception class that all other package exceptions inherit from? Does this let callers catch all package errors with one `except` clause?
168
+ - [ ] **Item 87 — Circular imports avoided** — Are there any circular imports between modules? Are they resolved by restructuring or by moving imports inside functions?
169
+ - [ ] **Item 88 — Virtual environments** — Is a `requirements.txt`, `Pipfile`, or `pyproject.toml` used to pin dependencies? Is the project developed in a virtual environment?
170
+
171
+ ---
172
+
173
+ ## Quick Review Workflow
174
+
175
+ 1. **Pythonic idiom pass** — Check f-strings, unpacking, enumerate, zip, comprehensions (Items 1-10, 27-36)
176
+ 2. **Function design** — Argument count, keyword-only args, return None vs. exceptions, functools.wraps (Items 19-26)
177
+ 3. **Class structure** — Composition vs. inheritance, super(), @property vs. plain attributes (Items 37-51)
178
+ 4. **Concurrency safety** — Thread-only for I/O, Lock, Queue, no blocking in async (Items 52-64)
179
+ 5. **Error handling** — try/except/else/finally, exception chaining, reraise (Items 65-67)
180
+ 6. **Test quality** — TestCase structure, mocks, one concept per test, testable design (Items 78-82)
181
+ 7. **Documentation** — Docstrings, package root exceptions, dependency management (Items 84-88)
182
+
183
+ ## Severity Levels
184
+
185
+ | Severity | Description | Examples |
186
+ |----------|-------------|---------|
187
+ | **Critical** | Correctness bugs or unsafe patterns | Threads for CPU parallelism (Item 53), blocking I/O in async (Item 61), mutable default arguments (Item 24), missing Lock on shared state (Item 54) |
188
+ | **High** | Maintainability violations that will cause pain | Returning None for errors (Item 20), missing super().__init__() (Item 40), no functools.wraps (Item 26), missing docstrings on public APIs (Item 84) |
189
+ | **Medium** | Un-Pythonic patterns that should be corrected | map/filter instead of comprehensions (Item 27), range(len()) instead of enumerate (Item 7), getter/setter instead of @property (Item 44) |
190
+ | **Low** | Polish and idiomatic improvements | Missing f-string (Item 4), index access instead of unpacking (Item 6), missing zip for parallel iteration (Item 8) |