@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.
Files changed (100) hide show
  1. package/CONTRIBUTING.md +122 -0
  2. package/README.md +20 -1
  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/SKILL.md +199 -0
  42. package/effective-python/evals/evals.json +44 -0
  43. package/effective-python/examples/after.md +56 -0
  44. package/effective-python/examples/before.md +40 -0
  45. package/effective-python/ref-01-pythonic-thinking.md +202 -0
  46. package/effective-python/ref-02-lists-and-dicts.md +146 -0
  47. package/effective-python/ref-03-functions.md +186 -0
  48. package/effective-python/ref-04-comprehensions-generators.md +211 -0
  49. package/effective-python/ref-05-classes-interfaces.md +188 -0
  50. package/effective-python/ref-06-metaclasses-attributes.md +209 -0
  51. package/effective-python/ref-07-concurrency.md +213 -0
  52. package/effective-python/ref-08-robustness-performance.md +248 -0
  53. package/effective-python/ref-09-testing-debugging.md +253 -0
  54. package/effective-python/ref-10-collaboration.md +175 -0
  55. package/effective-python/references/api_reference.md +218 -0
  56. package/effective-python/references/practices-catalog.md +483 -0
  57. package/effective-python/references/review-checklist.md +190 -0
  58. package/effective-python/scripts/lint.py +173 -0
  59. package/kotlin-in-action/evals/evals.json +43 -0
  60. package/kotlin-in-action/examples/after.md +53 -0
  61. package/kotlin-in-action/examples/before.md +39 -0
  62. package/kotlin-in-action/scripts/setup_detekt.py +224 -0
  63. package/lean-startup/evals/evals.json +43 -0
  64. package/lean-startup/examples/after.md +80 -0
  65. package/lean-startup/examples/before.md +34 -0
  66. package/lean-startup/scripts/new_experiment.py +286 -0
  67. package/microservices-patterns/SKILL.md +140 -0
  68. package/microservices-patterns/evals/evals.json +45 -0
  69. package/microservices-patterns/examples/after.md +69 -0
  70. package/microservices-patterns/examples/before.md +40 -0
  71. package/microservices-patterns/scripts/new_service.py +583 -0
  72. package/package.json +1 -1
  73. package/refactoring-ui/evals/evals.json +45 -0
  74. package/refactoring-ui/examples/after.md +85 -0
  75. package/refactoring-ui/examples/before.md +58 -0
  76. package/refactoring-ui/scripts/audit_css.py +250 -0
  77. package/skill-router/SKILL.md +142 -0
  78. package/skill-router/evals/evals.json +38 -0
  79. package/skill-router/examples/after.md +63 -0
  80. package/skill-router/examples/before.md +39 -0
  81. package/skill-router/references/api_reference.md +24 -0
  82. package/skill-router/references/routing-heuristics.md +89 -0
  83. package/skill-router/references/skill-catalog.md +156 -0
  84. package/skill-router/scripts/route.py +266 -0
  85. package/storytelling-with-data/evals/evals.json +47 -0
  86. package/storytelling-with-data/examples/after.md +50 -0
  87. package/storytelling-with-data/examples/before.md +33 -0
  88. package/storytelling-with-data/scripts/chart_review.py +301 -0
  89. package/system-design-interview/evals/evals.json +45 -0
  90. package/system-design-interview/examples/after.md +94 -0
  91. package/system-design-interview/examples/before.md +27 -0
  92. package/system-design-interview/scripts/new_design.py +421 -0
  93. package/using-asyncio-python/evals/evals.json +43 -0
  94. package/using-asyncio-python/examples/after.md +68 -0
  95. package/using-asyncio-python/examples/before.md +39 -0
  96. package/using-asyncio-python/scripts/check_blocking.py +270 -0
  97. package/web-scraping-python/evals/evals.json +46 -0
  98. package/web-scraping-python/examples/after.md +109 -0
  99. package/web-scraping-python/examples/before.md +40 -0
  100. package/web-scraping-python/scripts/new_scraper.py +231 -0
@@ -0,0 +1,211 @@
1
+ # Chapter 4: Comprehensions and Generators (Items 27-36)
2
+
3
+ ## Item 27: Use Comprehensions Instead of map and filter
4
+ ```python
5
+ # BAD
6
+ squares = map(lambda x: x**2, range(10))
7
+ even_squares = map(lambda x: x**2, filter(lambda x: x % 2 == 0, range(10)))
8
+
9
+ # GOOD
10
+ squares = [x**2 for x in range(10)]
11
+ even_squares = [x**2 for x in range(10) if x % 2 == 0]
12
+
13
+ # Also works for dicts and sets
14
+ chile_ranks = {rank: name for name, rank in names_and_ranks}
15
+ unique_lengths = {len(name) for name in names}
16
+ ```
17
+
18
+ ## Item 28: Avoid More Than Two Control Subexpressions in Comprehensions
19
+ ```python
20
+ # OK — two levels
21
+ flat = [x for row in matrix for x in row]
22
+
23
+ # OK — two conditions
24
+ filtered = [x for x in numbers if x > 0 if x % 2 == 0]
25
+
26
+ # BAD — too complex, hard to read
27
+ result = [x for sublist1 in my_lists
28
+ for sublist2 in sublist1
29
+ for x in sublist2]
30
+
31
+ # GOOD — use a loop or helper
32
+ result = []
33
+ for sublist1 in my_lists:
34
+ for sublist2 in sublist1:
35
+ result.extend(sublist2)
36
+ ```
37
+
38
+ - Rule of thumb: max two `for` subexpressions or two conditions
39
+ - Beyond that, use normal loops for readability
40
+
41
+ ## Item 29: Avoid Repeated Work in Comprehensions by Using Assignment Expressions
42
+ ```python
43
+ # BAD — calls get_batches twice
44
+ found = {name: batches for name in order
45
+ if (batches := get_batches(stock.get(name, 0), 8))}
46
+
47
+ # GOOD — walrus operator avoids repeated computation
48
+ found = {name: batches for name in order
49
+ if (batches := get_batches(stock.get(name, 0), 8))}
50
+
51
+ # The := expression in the condition makes 'batches' available in the value expression
52
+ ```
53
+
54
+ - Use `:=` in the `if` clause to compute once and reuse in the value expression
55
+ - The walrus variable leaks into the enclosing scope (be careful with naming)
56
+
57
+ ## Item 30: Consider Generators Instead of Returning Lists
58
+ ```python
59
+ # BAD — builds entire list in memory
60
+ def index_words(text):
61
+ result = []
62
+ if text:
63
+ result.append(0)
64
+ for index, letter in enumerate(text):
65
+ if letter == ' ':
66
+ result.append(index + 1)
67
+ return result
68
+
69
+ # GOOD — generator yields one at a time
70
+ def index_words(text):
71
+ if text:
72
+ yield 0
73
+ for index, letter in enumerate(text):
74
+ if letter == ' ':
75
+ yield index + 1
76
+ ```
77
+
78
+ - Generators use memory proportional to one output, not all outputs
79
+ - Use for large or infinite sequences
80
+ - Easy to convert: replace `result.append(x)` with `yield x`
81
+
82
+ ## Item 31: Be Defensive When Iterating Over Arguments
83
+ ```python
84
+ # BAD — generator exhausted after first iteration
85
+ def normalize(numbers):
86
+ total = sum(numbers) # exhausts the generator
87
+ result = []
88
+ for value in numbers: # nothing left to iterate!
89
+ result.append(value / total)
90
+ return result
91
+
92
+ # GOOD — accept an iterable container, not iterator
93
+ def normalize(numbers):
94
+ total = sum(numbers) # iterates once
95
+ result = []
96
+ for value in numbers: # iterates again — works with lists, not generators
97
+ result.append(value / total)
98
+ return result
99
+
100
+ # BETTER — use __iter__ protocol to detect single-use iterators
101
+ def normalize(numbers):
102
+ if iter(numbers) is numbers: # iterator, not container
103
+ raise TypeError('Must supply a container')
104
+ total = sum(numbers)
105
+ return [value / total for value in numbers]
106
+ ```
107
+
108
+ - Iterators are exhausted after one pass; containers are not
109
+ - Check `iter(x) is x` to detect iterators
110
+ - Or implement `__iter__` in a custom container class
111
+
112
+ ## Item 32: Consider Generator Expressions for Large List Comprehensions
113
+ ```python
114
+ # BAD — creates entire list in memory
115
+ values = [len(x) for x in open('my_file.txt')]
116
+
117
+ # GOOD — generator expression, lazy evaluation
118
+ values = (len(x) for x in open('my_file.txt'))
119
+
120
+ # Chain generator expressions
121
+ roots = ((x, x**0.5) for x in values)
122
+ ```
123
+
124
+ - Generator expressions use `()` instead of `[]`
125
+ - Lazy — only compute values as needed
126
+ - Can be chained together without memory overhead
127
+
128
+ ## Item 33: Compose Multiple Generators with yield from
129
+ ```python
130
+ # BAD — manual iteration
131
+ def chain_generators(gen1, gen2):
132
+ for item in gen1:
133
+ yield item
134
+ for item in gen2:
135
+ yield item
136
+
137
+ # GOOD — yield from
138
+ def chain_generators(gen1, gen2):
139
+ yield from gen1
140
+ yield from gen2
141
+
142
+ # Real example: tree traversal
143
+ def traverse(tree):
144
+ if tree is not None:
145
+ yield from traverse(tree.left)
146
+ yield tree.value
147
+ yield from traverse(tree.right)
148
+ ```
149
+
150
+ - `yield from` delegates to a sub-generator
151
+ - More readable and slightly faster than manual loop + yield
152
+
153
+ ## Item 34: Avoid Injecting Data into Generators with send
154
+ - `generator.send(value)` is complex and hard to understand
155
+ - Prefer passing an iterator to the generator instead
156
+ - Use `send` only when absolutely necessary (coroutine patterns)
157
+
158
+ ## Item 35: Avoid Causing State Transitions in Generators with throw
159
+ - `generator.throw(exception)` is confusing
160
+ - Use `__iter__` methods in a class instead for stateful iteration
161
+ - If you need exception handling in generators, prefer try/except inside the generator
162
+
163
+ ## Item 36: Consider itertools for Working with Iterators and Generators
164
+ **Linking iterators:**
165
+ ```python
166
+ import itertools
167
+
168
+ # Chain multiple iterators
169
+ itertools.chain(iter1, iter2)
170
+
171
+ # Repeat values
172
+ itertools.repeat('hello', 3)
173
+
174
+ # Cycle through an iterable
175
+ itertools.cycle([1, 2, 3])
176
+
177
+ # Parallel iteration with tee
178
+ it1, it2 = itertools.tee(iterator, 2)
179
+ ```
180
+
181
+ **Filtering:**
182
+ ```python
183
+ # takewhile — yield while predicate is True
184
+ itertools.takewhile(lambda x: x < 5, values)
185
+
186
+ # dropwhile — skip while predicate is True
187
+ itertools.dropwhile(lambda x: x < 5, values)
188
+
189
+ # filterfalse — yield items where predicate is False
190
+ itertools.filterfalse(lambda x: x < 5, values)
191
+
192
+ # islice — slice an iterator
193
+ itertools.islice(values, 2, 8, 2) # start, stop, step
194
+ ```
195
+
196
+ **Combining:**
197
+ ```python
198
+ # product — cartesian product
199
+ itertools.product([1,2], ['a','b']) # (1,'a'), (1,'b'), (2,'a'), (2,'b')
200
+
201
+ # permutations and combinations
202
+ itertools.permutations([1,2,3], 2)
203
+ itertools.combinations([1,2,3], 2)
204
+ itertools.combinations_with_replacement([1,2,3], 2)
205
+
206
+ # accumulate — running totals
207
+ itertools.accumulate([1,2,3,4]) # 1, 3, 6, 10
208
+
209
+ # zip_longest
210
+ itertools.zip_longest([1,2], [1,2,3], fillvalue=0)
211
+ ```
@@ -0,0 +1,188 @@
1
+ # Chapter 5: Classes and Interfaces (Items 37-43)
2
+
3
+ ## Item 37: Compose Classes Instead of Nesting Many Levels of Built-in Types
4
+ ```python
5
+ # BAD — deeply nested built-in types
6
+ grades = {} # dict of dict of list of tuples
7
+ grades['Math'] = {}
8
+ grades['Math']['test'] = [(95, 0.4), (87, 0.6)]
9
+
10
+ # GOOD — compose with named classes
11
+ from dataclasses import dataclass
12
+ from collections import namedtuple
13
+
14
+ Grade = namedtuple('Grade', ('score', 'weight'))
15
+
16
+ @dataclass
17
+ class Subject:
18
+ grades: list
19
+
20
+ def average_grade(self):
21
+ total = sum(g.score * g.weight for g in self.grades)
22
+ total_weight = sum(g.weight for g in self.grades)
23
+ return total / total_weight
24
+
25
+ @dataclass
26
+ class Student:
27
+ subjects: dict # name -> Subject
28
+
29
+ class Gradebook:
30
+ def __init__(self):
31
+ self._students = {}
32
+ ```
33
+
34
+ - When nesting goes beyond dict of dict, refactor into classes
35
+ - Use `namedtuple` for lightweight immutable data containers
36
+ - Use `dataclass` for mutable data containers with behavior
37
+ - Bottom-up refactoring: start with the innermost type
38
+
39
+ ## Item 38: Accept Functions Instead of Classes for Simple Interfaces
40
+ ```python
41
+ # Python's hooks can accept any callable
42
+ names = ['Socrates', 'Archimedes', 'Plato']
43
+ names.sort(key=len) # function as interface
44
+
45
+ # Use __call__ for stateful callables
46
+ class CountMissing:
47
+ def __init__(self):
48
+ self.added = 0
49
+
50
+ def __call__(self):
51
+ self.added += 1
52
+ return 0
53
+
54
+ counter = CountMissing()
55
+ result = defaultdict(counter, current_data) # uses __call__
56
+ print(counter.added)
57
+ ```
58
+
59
+ - Functions are first-class in Python — use them as interfaces
60
+ - For stateful behavior, define `__call__` on a class
61
+ - Simpler than defining full interface classes
62
+
63
+ ## Item 39: Use @classmethod Polymorphism to Construct Objects Generically
64
+ ```python
65
+ class InputData:
66
+ def read(self):
67
+ raise NotImplementedError
68
+
69
+ class PathInputData(InputData):
70
+ def __init__(self, path):
71
+ self.path = path
72
+
73
+ def read(self):
74
+ return open(self.path).read()
75
+
76
+ @classmethod
77
+ def generate_inputs(cls, config):
78
+ """Factory that creates instances from config."""
79
+ data_dir = config['data_dir']
80
+ for name in os.listdir(data_dir):
81
+ yield cls(os.path.join(data_dir, name))
82
+ ```
83
+
84
+ - Use `@classmethod` as a polymorphic constructor
85
+ - Enables subclasses to provide their own construction logic
86
+ - Avoids hardcoding class names in factory functions
87
+
88
+ ## Item 40: Initialize Parent Classes with super()
89
+ ```python
90
+ # BAD — direct call to parent
91
+ class Child(Parent):
92
+ def __init__(self):
93
+ Parent.__init__(self) # breaks with multiple inheritance
94
+
95
+ # GOOD — always use super()
96
+ class Child(Parent):
97
+ def __init__(self):
98
+ super().__init__()
99
+ ```
100
+
101
+ - `super()` follows the MRO (Method Resolution Order) correctly
102
+ - Essential for multiple inheritance (diamond problem)
103
+ - Always call `super().__init__()` in `__init__` methods
104
+ - The MRO is deterministic: use `ClassName.__mro__` or `ClassName.mro()` to inspect
105
+
106
+ ## Item 41: Consider Composing Functionality with Mix-in Classes
107
+ ```python
108
+ # Mix-in: a class that provides extra functionality without its own state
109
+ class JsonMixin:
110
+ @classmethod
111
+ def from_json(cls, data):
112
+ kwargs = json.loads(data)
113
+ return cls(**kwargs)
114
+
115
+ def to_json(self):
116
+ return json.dumps(self.__dict__)
117
+
118
+ class DatacenterRack(JsonMixin):
119
+ def __init__(self, switch=None, machines=None):
120
+ self.switch = switch
121
+ self.machines = machines
122
+
123
+ # Usage
124
+ rack = DatacenterRack.from_json(json_data)
125
+ json_str = rack.to_json()
126
+ ```
127
+
128
+ - Mix-ins provide reusable behavior without instance state
129
+ - Classes can use multiple mix-ins via multiple inheritance
130
+ - Prefer mix-ins over deep inheritance hierarchies
131
+ - Name them with `Mixin` suffix for clarity
132
+
133
+ ## Item 42: Prefer Public Attributes Over Private Ones
134
+ ```python
135
+ # BAD — private attributes (__name mangling)
136
+ class MyObject:
137
+ def __init__(self):
138
+ self.__private_field = 10 # name-mangled to _MyObject__private_field
139
+
140
+ # GOOD — protected with convention
141
+ class MyObject:
142
+ def __init__(self):
143
+ self._protected_field = 10 # convention: internal use
144
+
145
+ # Access is still possible but signals "internal"
146
+ obj = MyObject()
147
+ obj._protected_field # works, but callers know it's internal
148
+ ```
149
+
150
+ - `__double_underscore` causes name mangling — don't use it
151
+ - Use `_single_underscore` for protected/internal attributes
152
+ - Python philosophy: "We're all consenting adults"
153
+ - Name mangling breaks subclass access and makes debugging harder
154
+ - Only use `__` to avoid naming conflicts with subclasses (rare)
155
+
156
+ ## Item 43: Inherit from collections.abc for Custom Container Types
157
+ ```python
158
+ from collections.abc import Sequence
159
+
160
+ class FrequencyList(list):
161
+ def frequency(self):
162
+ counts = {}
163
+ for item in self:
164
+ counts[item] = counts.get(item, 0) + 1
165
+ return counts
166
+
167
+ # For custom containers, inherit from collections.abc
168
+ class BinaryNode(Sequence):
169
+ def __init__(self, value, left=None, right=None):
170
+ self.value = value
171
+ self.left = left
172
+ self.right = right
173
+
174
+ def __getitem__(self, index):
175
+ # Required by Sequence
176
+ ...
177
+
178
+ def __len__(self):
179
+ # Required by Sequence
180
+ ...
181
+
182
+ # count() and index() provided automatically by Sequence
183
+ ```
184
+
185
+ - `collections.abc` provides abstract base classes for containers
186
+ - Inheriting ensures you implement required methods
187
+ - You get mixin methods for free (e.g., `count`, `index` from `Sequence`)
188
+ - Available ABCs: `Sequence`, `MutableSequence`, `Set`, `MutableSet`, `Mapping`, `MutableMapping`, etc.
@@ -0,0 +1,209 @@
1
+ # Chapter 6: Metaclasses and Attributes (Items 44-51)
2
+
3
+ ## Item 44: Use Plain Attributes Instead of Setter and Getter Methods
4
+ ```python
5
+ # BAD — Java-style getters/setters
6
+ class OldResistor:
7
+ def __init__(self, ohms):
8
+ self._ohms = ohms
9
+
10
+ def get_ohms(self):
11
+ return self._ohms
12
+
13
+ def set_ohms(self, ohms):
14
+ self._ohms = ohms
15
+
16
+ # GOOD — plain attributes
17
+ class Resistor:
18
+ def __init__(self, ohms):
19
+ self.ohms = ohms
20
+
21
+ # If you later need behavior, migrate to @property (Item 44)
22
+ ```
23
+
24
+ - Start with simple public attributes
25
+ - If you need special behavior later, use `@property` without changing the API
26
+ - Never write explicit getter/setter methods in Python
27
+
28
+ ## Item 45: Consider @property Instead of Refactoring Attributes
29
+ ```python
30
+ class Bucket:
31
+ def __init__(self, period):
32
+ self.period = period
33
+ self.quota = 0
34
+
35
+ @property
36
+ def quota(self):
37
+ return self._quota
38
+
39
+ @quota.setter
40
+ def quota(self, value):
41
+ if value < 0:
42
+ raise ValueError('Quota must be >= 0')
43
+ self._quota = value
44
+ ```
45
+
46
+ - Use `@property` to add validation, logging, or computed behavior
47
+ - Keeps backward-compatible API (attribute access syntax)
48
+ - Don't do too much work in property getters — keep them fast
49
+ - If a property is getting complex, refactor to a normal method
50
+
51
+ ## Item 46: Use Descriptors for Reusable @property Methods
52
+ ```python
53
+ class Grade:
54
+ """Reusable validation descriptor."""
55
+ def __init__(self):
56
+ self._values = {}
57
+
58
+ def __get__(self, instance, instance_type):
59
+ if instance is None:
60
+ return self
61
+ return self._values.get(instance, 0)
62
+
63
+ def __set__(self, instance, value):
64
+ if not (0 <= value <= 100):
65
+ raise ValueError('Grade must be between 0 and 100')
66
+ self._values[instance] = value
67
+
68
+ class Exam:
69
+ math_grade = Grade()
70
+ writing_grade = Grade()
71
+ science_grade = Grade()
72
+
73
+ exam = Exam()
74
+ exam.math_grade = 95 # calls Grade.__set__
75
+ print(exam.math_grade) # calls Grade.__get__
76
+ ```
77
+
78
+ - Use descriptors when you'd copy-paste `@property` logic
79
+ - Store per-instance data using `WeakKeyDictionary` to avoid memory leaks:
80
+ ```python
81
+ from weakref import WeakKeyDictionary
82
+ class Grade:
83
+ def __init__(self):
84
+ self._values = WeakKeyDictionary()
85
+ ```
86
+
87
+ ## Item 47: Use __getattr__, __getattribute__, and __setattr__ for Lazy Attributes
88
+ ```python
89
+ # __getattr__ — called only when attribute not found normally
90
+ class LazyRecord:
91
+ def __init__(self):
92
+ self.exists = 5
93
+
94
+ def __getattr__(self, name):
95
+ value = f'Value for {name}'
96
+ setattr(self, name, value) # cache it
97
+ return value
98
+
99
+ # __getattribute__ — called for EVERY attribute access
100
+ class ValidatingRecord:
101
+ def __getattribute__(self, name):
102
+ value = super().__getattribute__(name)
103
+ # validate or log every access
104
+ return value
105
+
106
+ # __setattr__ — called for EVERY attribute assignment
107
+ class SavingRecord:
108
+ def __setattr__(self, name, value):
109
+ super().__setattr__(name, value)
110
+ # save to database, etc.
111
+ ```
112
+
113
+ - `__getattr__` is for lazy/dynamic attributes (called only on missing)
114
+ - `__getattribute__` intercepts ALL attribute access (use carefully)
115
+ - Always use `super()` in these methods to avoid infinite recursion
116
+ - `hasattr` and `getattr` also trigger `__getattribute__`
117
+
118
+ ## Item 48: Validate Subclasses with __init_subclass__
119
+ ```python
120
+ class Polygon:
121
+ sides = None
122
+
123
+ def __init_subclass__(cls, **kwargs):
124
+ super().__init_subclass__(**kwargs)
125
+ if cls.sides is None or cls.sides < 3:
126
+ raise ValueError('Polygons need 3+ sides')
127
+
128
+ class Triangle(Polygon):
129
+ sides = 3 # OK
130
+
131
+ class Line(Polygon):
132
+ sides = 2 # Raises ValueError at class definition time!
133
+ ```
134
+
135
+ - `__init_subclass__` is called when a class is subclassed
136
+ - Use it for validation, registration, or class setup
137
+ - Much simpler than metaclasses for most use cases
138
+ - Works with multiple inheritance (use `**kwargs` to pass through)
139
+
140
+ ## Item 49: Register Class Existence with __init_subclass__
141
+ ```python
142
+ registry = {}
143
+
144
+ class Serializable:
145
+ def __init_subclass__(cls, **kwargs):
146
+ super().__init_subclass__(**kwargs)
147
+ registry[cls.__name__] = cls
148
+
149
+ class Point(Serializable):
150
+ def __init__(self, x, y):
151
+ self.x = x
152
+ self.y = y
153
+
154
+ # Point is automatically registered
155
+ assert registry['Point'] is Point
156
+ ```
157
+
158
+ - Auto-registration pattern: base class registers all subclasses
159
+ - Useful for serialization, plugin systems, ORM models
160
+ - Replaces the need for explicit registration decorators or metaclasses
161
+
162
+ ## Item 50: Annotate Class Attributes with __set_name__
163
+ ```python
164
+ class Field:
165
+ def __set_name__(self, owner, name):
166
+ self.name = name # attribute name on the class
167
+ self.internal_name = '_' + name # storage name
168
+
169
+ def __get__(self, instance, instance_type):
170
+ if instance is None:
171
+ return self
172
+ return getattr(instance, self.internal_name, '')
173
+
174
+ def __set__(self, instance, value):
175
+ setattr(instance, self.internal_name, value)
176
+
177
+ class Customer:
178
+ first_name = Field() # __set_name__ called with name='first_name'
179
+ last_name = Field()
180
+ ```
181
+
182
+ - `__set_name__` is called automatically when a descriptor is assigned to a class attribute
183
+ - Eliminates the need to repeat the attribute name
184
+ - Works with descriptors to provide clean, DRY class definitions
185
+
186
+ ## Item 51: Prefer Class Decorators Over Metaclasses for Composable Class Extensions
187
+ ```python
188
+ # Class decorator — simple and composable
189
+ def my_class_decorator(cls):
190
+ # modify or wrap cls
191
+ original_init = cls.__init__
192
+
193
+ def new_init(self, *args, **kwargs):
194
+ print(f'Creating {cls.__name__}')
195
+ original_init(self, *args, **kwargs)
196
+
197
+ cls.__init__ = new_init
198
+ return cls
199
+
200
+ @my_class_decorator
201
+ class MyClass:
202
+ def __init__(self, value):
203
+ self.value = value
204
+ ```
205
+
206
+ - Class decorators are simpler than metaclasses
207
+ - They compose easily (stack multiple decorators)
208
+ - Use metaclasses only when you need to control the class creation process itself
209
+ - Prefer: `__init_subclass__` > class decorators > metaclasses