@booklib/skills 1.0.0 → 1.3.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/CONTRIBUTING.md +122 -0
- package/README.md +20 -1
- package/ROADMAP.md +36 -0
- package/animation-at-work/evals/evals.json +44 -0
- package/animation-at-work/examples/after.md +64 -0
- package/animation-at-work/examples/before.md +35 -0
- package/animation-at-work/scripts/audit_animations.py +295 -0
- package/bin/skills.js +552 -42
- package/clean-code-reviewer/SKILL.md +109 -1
- package/clean-code-reviewer/evals/evals.json +121 -3
- package/clean-code-reviewer/examples/after.md +48 -0
- package/clean-code-reviewer/examples/before.md +33 -0
- package/clean-code-reviewer/references/api_reference.md +158 -0
- package/clean-code-reviewer/references/practices-catalog.md +282 -0
- package/clean-code-reviewer/references/review-checklist.md +254 -0
- package/clean-code-reviewer/scripts/pre-review.py +206 -0
- package/data-intensive-patterns/evals/evals.json +43 -0
- package/data-intensive-patterns/examples/after.md +61 -0
- package/data-intensive-patterns/examples/before.md +38 -0
- package/data-intensive-patterns/scripts/adr.py +213 -0
- package/data-pipelines/evals/evals.json +45 -0
- package/data-pipelines/examples/after.md +97 -0
- package/data-pipelines/examples/before.md +37 -0
- package/data-pipelines/scripts/new_pipeline.py +444 -0
- package/design-patterns/evals/evals.json +46 -0
- package/design-patterns/examples/after.md +52 -0
- package/design-patterns/examples/before.md +29 -0
- package/design-patterns/scripts/scaffold.py +807 -0
- package/domain-driven-design/SKILL.md +120 -0
- package/domain-driven-design/evals/evals.json +48 -0
- package/domain-driven-design/examples/after.md +80 -0
- package/domain-driven-design/examples/before.md +43 -0
- package/domain-driven-design/scripts/scaffold.py +421 -0
- package/effective-java/evals/evals.json +46 -0
- package/effective-java/examples/after.md +83 -0
- package/effective-java/examples/before.md +37 -0
- package/effective-java/scripts/checkstyle_setup.py +211 -0
- package/effective-kotlin/evals/evals.json +45 -0
- package/effective-kotlin/examples/after.md +36 -0
- package/effective-kotlin/examples/before.md +38 -0
- package/effective-python/SKILL.md +199 -0
- package/effective-python/evals/evals.json +44 -0
- package/effective-python/examples/after.md +56 -0
- package/effective-python/examples/before.md +40 -0
- package/effective-python/ref-01-pythonic-thinking.md +202 -0
- package/effective-python/ref-02-lists-and-dicts.md +146 -0
- package/effective-python/ref-03-functions.md +186 -0
- package/effective-python/ref-04-comprehensions-generators.md +211 -0
- package/effective-python/ref-05-classes-interfaces.md +188 -0
- package/effective-python/ref-06-metaclasses-attributes.md +209 -0
- package/effective-python/ref-07-concurrency.md +213 -0
- package/effective-python/ref-08-robustness-performance.md +248 -0
- package/effective-python/ref-09-testing-debugging.md +253 -0
- package/effective-python/ref-10-collaboration.md +175 -0
- package/effective-python/references/api_reference.md +218 -0
- package/effective-python/references/practices-catalog.md +483 -0
- package/effective-python/references/review-checklist.md +190 -0
- package/effective-python/scripts/lint.py +173 -0
- package/kotlin-in-action/evals/evals.json +43 -0
- package/kotlin-in-action/examples/after.md +53 -0
- package/kotlin-in-action/examples/before.md +39 -0
- package/kotlin-in-action/scripts/setup_detekt.py +224 -0
- package/lean-startup/evals/evals.json +43 -0
- package/lean-startup/examples/after.md +80 -0
- package/lean-startup/examples/before.md +34 -0
- package/lean-startup/scripts/new_experiment.py +286 -0
- package/microservices-patterns/SKILL.md +140 -0
- package/microservices-patterns/evals/evals.json +45 -0
- package/microservices-patterns/examples/after.md +69 -0
- package/microservices-patterns/examples/before.md +40 -0
- package/microservices-patterns/scripts/new_service.py +583 -0
- package/package.json +1 -1
- package/refactoring-ui/evals/evals.json +45 -0
- package/refactoring-ui/examples/after.md +85 -0
- package/refactoring-ui/examples/before.md +58 -0
- package/refactoring-ui/scripts/audit_css.py +250 -0
- package/skill-router/SKILL.md +142 -0
- package/skill-router/evals/evals.json +38 -0
- package/skill-router/examples/after.md +63 -0
- package/skill-router/examples/before.md +39 -0
- package/skill-router/references/api_reference.md +24 -0
- package/skill-router/references/routing-heuristics.md +89 -0
- package/skill-router/references/skill-catalog.md +156 -0
- package/skill-router/scripts/route.py +266 -0
- package/storytelling-with-data/evals/evals.json +47 -0
- package/storytelling-with-data/examples/after.md +50 -0
- package/storytelling-with-data/examples/before.md +33 -0
- package/storytelling-with-data/scripts/chart_review.py +301 -0
- package/system-design-interview/evals/evals.json +45 -0
- package/system-design-interview/examples/after.md +94 -0
- package/system-design-interview/examples/before.md +27 -0
- package/system-design-interview/scripts/new_design.py +421 -0
- package/using-asyncio-python/evals/evals.json +43 -0
- package/using-asyncio-python/examples/after.md +68 -0
- package/using-asyncio-python/examples/before.md +39 -0
- package/using-asyncio-python/scripts/check_blocking.py +270 -0
- package/web-scraping-python/evals/evals.json +46 -0
- package/web-scraping-python/examples/after.md +109 -0
- package/web-scraping-python/examples/before.md +40 -0
- package/web-scraping-python/scripts/new_scraper.py +231 -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) |
|