@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,40 @@
1
+ # Before
2
+
3
+ Python ETL helper with a bare `except`, a mutable default argument bug, and a manual loop that should be a comprehension.
4
+
5
+ ```python
6
+ import json
7
+ import requests
8
+
9
+ def fetch_orders(api_url, filters={}):
10
+ # filters dict persists across calls — mutable default arg bug
11
+ filters['status'] = 'completed'
12
+ try:
13
+ response = requests.get(api_url, params=filters)
14
+ data = response.json()
15
+ except:
16
+ # swallows every exception including KeyboardInterrupt
17
+ print("something went wrong")
18
+ return []
19
+
20
+ orders = []
21
+ for item in data['orders']:
22
+ if item['total'] > 0:
23
+ order = {
24
+ 'id': item['id'],
25
+ 'customer': item['customer_name'],
26
+ 'total': item['total'],
27
+ }
28
+ orders.append(order)
29
+ return orders
30
+
31
+
32
+ def summarize(orders):
33
+ totals = []
34
+ for o in orders:
35
+ totals.append(o['total'])
36
+ grand_total = 0
37
+ for t in totals:
38
+ grand_total = grand_total + t
39
+ return grand_total
40
+ ```
@@ -0,0 +1,202 @@
1
+ # Chapter 1: Pythonic Thinking (Items 1-10)
2
+
3
+ ## Item 1: Know Which Version of Python You're Using
4
+ - Use `python3` explicitly, not `python`
5
+ - Check version with `python3 --version` or `sys.version_info`
6
+ - Python 2 is end-of-life; always target Python 3
7
+
8
+ ## Item 2: Follow the PEP 8 Style Guide
9
+ **Whitespace:**
10
+ - Use 4 spaces for indentation (never tabs)
11
+ - Lines should be 79 characters or fewer
12
+ - Continuations should be indented by 4 extra spaces
13
+ - Put two blank lines before/after top-level functions and classes
14
+ - One blank line between methods in a class
15
+
16
+ **Naming:**
17
+ - Functions, variables, attributes: `lowercase_underscore`
18
+ - Protected instance attributes: `_leading_underscore`
19
+ - Private instance attributes: `__double_leading_underscore`
20
+ - Classes and exceptions: `CapitalizedWord`
21
+ - Module-level constants: `ALL_CAPS`
22
+ - Instance methods use `self` as first parameter; class methods use `cls`
23
+
24
+ **Expressions & Statements:**
25
+ - Use inline negation (`if a is not b`) instead of negating positive (`if not a is b`)
26
+ - Don't check for empty containers with length (`if len(list) == 0`); use `if not list`
27
+ - Use `if list` to check for non-empty
28
+ - Avoid single-line `if`, `for`, `while`, `except`
29
+ - Always use absolute imports, not relative
30
+ - Put imports at top in order: stdlib, third-party, local
31
+
32
+ **Tools:** Use `pylint` for static analysis, `black` for formatting.
33
+
34
+ ## Item 3: Know the Differences Between bytes and str
35
+ - `bytes` contains raw unsigned 8-bit values; `str` contains Unicode code points
36
+ - Use helper functions to convert between them:
37
+
38
+ ```python
39
+ # BAD
40
+ def to_str(data):
41
+ if isinstance(data, bytes):
42
+ return data.decode('utf-8')
43
+ return data
44
+
45
+ # GOOD — be explicit about encoding
46
+ def to_str(bytes_or_str):
47
+ if isinstance(bytes_or_str, bytes):
48
+ value = bytes_or_str.decode('utf-8')
49
+ else:
50
+ value = bytes_or_str
51
+ return value
52
+
53
+ def to_bytes(bytes_or_str):
54
+ if isinstance(bytes_or_str, str):
55
+ value = bytes_or_str.encode('utf-8')
56
+ else:
57
+ value = bytes_or_str
58
+ return value
59
+ ```
60
+
61
+ - Use `'rb'` and `'wb'` modes for binary file I/O
62
+ - Specify encoding explicitly: `open(path, 'r', encoding='utf-8')`
63
+
64
+ ## Item 4: Prefer Interpolated F-Strings Over C-style Format Strings and str.format
65
+ ```python
66
+ # BAD — C-style
67
+ 'Hello, %s. You are %d.' % (name, age)
68
+
69
+ # BAD — str.format
70
+ 'Hello, {}. You are {}.'.format(name, age)
71
+
72
+ # GOOD — f-string
73
+ f'Hello, {name}. You are {age}.'
74
+
75
+ # F-strings support expressions
76
+ f'{key!r}: {value:.2f}'
77
+ f'result: {some_func(x)}'
78
+
79
+ # Multi-line f-strings
80
+ f'{key:<10} = {value:.2f}'
81
+ ```
82
+
83
+ ## Item 5: Write Helper Functions Instead of Complex Expressions
84
+ - If an expression is hard to read, move it to a helper function
85
+ - Clarity over brevity: `if`/`else` is clearer than `or` for defaults
86
+
87
+ ```python
88
+ # BAD
89
+ values = query_string.get('red', [''])
90
+ red = int(values[0]) if values[0] else 0
91
+
92
+ # GOOD
93
+ def get_first_int(values, key, default=0):
94
+ found = values.get(key, [''])
95
+ if found[0]:
96
+ return int(found[0])
97
+ return default
98
+
99
+ red = get_first_int(values, 'red')
100
+ ```
101
+
102
+ ## Item 6: Prefer Multiple Assignment Unpacking Over Indexing
103
+ ```python
104
+ # BAD
105
+ item = ('Peanut Butter', 3.50)
106
+ name = item[0]
107
+ price = item[1]
108
+
109
+ # GOOD
110
+ name, price = item
111
+
112
+ # Works with nested structures
113
+ ((name1, cal1), (name2, cal2)) = snacks
114
+
115
+ # Use _ for unused values
116
+ _, price = item
117
+
118
+ # Swap without temp variable
119
+ a, b = b, a
120
+ ```
121
+
122
+ ## Item 7: Prefer enumerate Over range
123
+ ```python
124
+ # BAD
125
+ for i in range(len(flavor_list)):
126
+ flavor = flavor_list[i]
127
+ print(f'{i + 1}: {flavor}')
128
+
129
+ # GOOD
130
+ for i, flavor in enumerate(flavor_list):
131
+ print(f'{i + 1}: {flavor}')
132
+
133
+ # Start from a different index
134
+ for i, flavor in enumerate(flavor_list, 1):
135
+ print(f'{i}: {flavor}')
136
+ ```
137
+
138
+ ## Item 8: Use zip to Process Iterators in Parallel
139
+ ```python
140
+ # BAD
141
+ for i in range(len(names)):
142
+ print(f'{names[i]}: {counts[i]}')
143
+
144
+ # GOOD
145
+ for name, count in zip(names, counts):
146
+ print(f'{name}: {count}')
147
+
148
+ # When lengths differ, use zip_longest
149
+ from itertools import zip_longest
150
+ for name, count in zip_longest(names, counts, fillvalue=0):
151
+ print(f'{name}: {count}')
152
+ ```
153
+
154
+ - `zip` truncates to shortest iterator (use `itertools.zip_longest` if needed)
155
+ - zip is lazy — produces one tuple at a time
156
+
157
+ ## Item 9: Avoid else Blocks After for and while Loops
158
+ - `else` on loops runs when the loop completes *without* `break`
159
+ - This is counterintuitive and confuses readers
160
+ - Instead, use a helper function with early return:
161
+
162
+ ```python
163
+ # BAD — confusing else on loop
164
+ for i in range(n):
165
+ if condition(i):
166
+ break
167
+ else:
168
+ handle_no_break()
169
+
170
+ # GOOD — helper function
171
+ def find_match(n):
172
+ for i in range(n):
173
+ if condition(i):
174
+ return i
175
+ return None # explicit "not found"
176
+
177
+ result = find_match(n)
178
+ if result is None:
179
+ handle_no_match()
180
+ ```
181
+
182
+ ## Item 10: Prevent Repetition with Assignment Expressions (Walrus Operator)
183
+ ```python
184
+ # BAD — repeated call or extra variable
185
+ count = fresh_fruit.get('lemon', 0)
186
+ if count:
187
+ make_lemonade(count)
188
+
189
+ # GOOD — walrus operator
190
+ if count := fresh_fruit.get('lemon', 0):
191
+ make_lemonade(count)
192
+
193
+ # Useful in while loops
194
+ while chunk := f.read(8192):
195
+ process(chunk)
196
+
197
+ # Useful in comprehensions
198
+ result = [y for x in data if (y := f(x)) is not None]
199
+ ```
200
+
201
+ - Use `:=` when you need to both assign and test a value
202
+ - Don't overuse — only when it clearly reduces repetition
@@ -0,0 +1,146 @@
1
+ # Chapter 2: Lists and Dictionaries (Items 11-18)
2
+
3
+ ## Item 11: Know How to Slice Sequences
4
+ ```python
5
+ a = [1, 2, 3, 4, 5, 6, 7, 8]
6
+
7
+ # Basic slicing
8
+ a[:4] # [1, 2, 3, 4] — first 4
9
+ a[-3:] # [6, 7, 8] — last 3
10
+ a[3:5] # [4, 5]
11
+
12
+ # Don't use 0 for start or len for end
13
+ a[:5] # GOOD
14
+ a[0:5] # BAD — redundant 0
15
+
16
+ # Slicing makes a new list (shallow copy)
17
+ b = a[:] # copy of a
18
+
19
+ # Slice assignment replaces in place
20
+ a[2:4] = [10, 11] # can be different length
21
+ ```
22
+
23
+ ## Item 12: Avoid Striding and Slicing in a Single Expression
24
+ ```python
25
+ # BAD — confusing stride + slice
26
+ x = a[2::2] # skip start, stride by 2
27
+ x = a[-2::-2] # reverse with stride
28
+
29
+ # GOOD — separate steps
30
+ y = a[::2] # stride first
31
+ z = y[1:3] # then slice
32
+
33
+ # Reverse a sequence
34
+ x = a[::-1] # OK for simple reversal, but avoid combining with slicing
35
+ ```
36
+
37
+ ## Item 13: Prefer Catch-All Unpacking Over Slicing
38
+ ```python
39
+ # BAD — manual slicing
40
+ oldest = ages[0]
41
+ rest = ages[1:]
42
+
43
+ # GOOD — starred expression
44
+ oldest, *rest = ages
45
+ oldest, second, *rest = ages
46
+ first, *middle, last = ages
47
+
48
+ # Works with any iterable
49
+ it = iter(range(10))
50
+ first, second, *rest = it
51
+ ```
52
+
53
+ - Starred expressions always produce a list (may be empty)
54
+ - Cannot have more than one starred expression in a single assignment
55
+
56
+ ## Item 14: Sort by Complex Criteria Using the key Parameter
57
+ ```python
58
+ # Sort with key function
59
+ tools = [Tool('drill', 4), Tool('saw', 2)]
60
+ tools.sort(key=lambda x: x.weight)
61
+
62
+ # Multiple criteria — use tuple
63
+ tools.sort(key=lambda x: (x.name, x.weight))
64
+
65
+ # Reverse one criterion using negation (numeric)
66
+ tools.sort(key=lambda x: (-x.weight, x.name))
67
+
68
+ # For non-numeric reverse, use multiple sort passes (stable sort)
69
+ tools.sort(key=lambda x: x.name) # secondary first
70
+ tools.sort(key=lambda x: x.weight, reverse=True) # primary last
71
+ ```
72
+
73
+ - Python sort is stable — equal elements maintain relative order
74
+ - Use `operator.attrgetter` for attribute access as key
75
+
76
+ ## Item 15: Be Cautious When Relying on dict Insertion Order
77
+ - Since Python 3.7, dicts maintain insertion order
78
+ - But don't assume all dict-like objects do (e.g., custom classes)
79
+ - Use explicit ordering when you need it:
80
+
81
+ ```python
82
+ # If order matters and you're creating a protocol
83
+ class MyDB:
84
+ def __init__(self):
85
+ self._data = {}
86
+
87
+ # Be explicit that order is part of the contract
88
+ ```
89
+
90
+ - For **kwargs, insertion order is preserved
91
+ - Standard dict methods (keys, values, items) follow insertion order
92
+
93
+ ## Item 16: Prefer get Over in and KeyError to Handle Missing Dictionary Keys
94
+ ```python
95
+ # BAD — check then access
96
+ if key in counters:
97
+ count = counters[key]
98
+ else:
99
+ count = 0
100
+ counters[key] = count + 1
101
+
102
+ # BAD — try/except
103
+ try:
104
+ count = counters[key]
105
+ except KeyError:
106
+ count = 0
107
+ counters[key] = count + 1
108
+
109
+ # GOOD — use get
110
+ count = counters.get(key, 0)
111
+ counters[key] = count + 1
112
+
113
+ # For complex default values, consider setdefault or defaultdict
114
+ ```
115
+
116
+ ## Item 17: Prefer defaultdict Over setdefault to Handle Missing Items in Internal State
117
+ ```python
118
+ from collections import defaultdict
119
+
120
+ # BAD — setdefault (confusing API)
121
+ visits = {}
122
+ visits.setdefault('France', []).append('Paris')
123
+
124
+ # GOOD — defaultdict
125
+ visits = defaultdict(list)
126
+ visits['France'].append('Paris')
127
+ ```
128
+
129
+ - `defaultdict` is clearer when you control the dict creation
130
+ - `setdefault` is better when you don't control the dict (external data)
131
+
132
+ ## Item 18: Know How to Construct Key-Dependent Default Values with __missing__
133
+ ```python
134
+ # When the default value depends on the key, use __missing__
135
+ class Pictures(dict):
136
+ def __missing__(self, key):
137
+ value = open_picture(key) # default depends on key
138
+ self[key] = value
139
+ return value
140
+
141
+ pictures = Pictures()
142
+ handle = pictures[path] # calls __missing__ if path not present
143
+ ```
144
+
145
+ - Use when `defaultdict` isn't sufficient (default factory doesn't receive the key)
146
+ - `__missing__` is called by `__getitem__` when key is not found
@@ -0,0 +1,186 @@
1
+ # Chapter 3: Functions (Items 19-26)
2
+
3
+ ## Item 19: Never Unpack More Than Three Variables When Functions Return Multiple Values
4
+ ```python
5
+ # BAD — too many unpacked values, confusing
6
+ minimum, maximum, average, median, count = get_stats(data)
7
+
8
+ # GOOD — return a lightweight class or namedtuple
9
+ from collections import namedtuple
10
+
11
+ Stats = namedtuple('Stats', ['minimum', 'maximum', 'average', 'median', 'count'])
12
+
13
+ def get_stats(data):
14
+ return Stats(
15
+ minimum=min(data),
16
+ maximum=max(data),
17
+ average=sum(data)/len(data),
18
+ median=find_median(data),
19
+ count=len(data)
20
+ )
21
+
22
+ result = get_stats(data)
23
+ print(result.average)
24
+ ```
25
+
26
+ - Three or fewer values is fine to unpack
27
+ - More than three: use a namedtuple, dataclass, or custom class
28
+
29
+ ## Item 20: Prefer Raising Exceptions to Returning None
30
+ ```python
31
+ # BAD — None is ambiguous
32
+ def careful_divide(a, b):
33
+ try:
34
+ return a / b
35
+ except ZeroDivisionError:
36
+ return None
37
+
38
+ # Caller can't distinguish None from 0
39
+ result = careful_divide(0, 5) # returns 0.0
40
+ if not result: # BUG: treats 0.0 as failure
41
+
42
+ # GOOD — raise an exception
43
+ def careful_divide(a, b):
44
+ try:
45
+ return a / b
46
+ except ZeroDivisionError as e:
47
+ raise ValueError('Invalid inputs') from e
48
+
49
+ # ALSO GOOD — type hints with Never-None return
50
+ def careful_divide(a: float, b: float) -> float:
51
+ """Raises ValueError on invalid inputs."""
52
+ if b == 0:
53
+ raise ValueError('Invalid inputs')
54
+ return a / b
55
+ ```
56
+
57
+ ## Item 21: Know How Closures Interact with Variable Scope
58
+ ```python
59
+ # Closures capture variables from enclosing scope
60
+ def sort_priority(values, group):
61
+ found = False
62
+ def helper(x):
63
+ nonlocal found # REQUIRED to modify enclosing variable
64
+ if x in group:
65
+ found = True
66
+ return (0, x)
67
+ return (1, x)
68
+ values.sort(key=helper)
69
+ return found
70
+ ```
71
+
72
+ - Without `nonlocal`, assignment creates a new local variable
73
+ - Prefer returning state over using `nonlocal` for complex cases
74
+ - For complex state, use a helper class instead
75
+
76
+ ## Item 22: Reduce Visual Noise with Variable Positional Arguments (*args)
77
+ ```python
78
+ # Accept variable args
79
+ def log(message, *values):
80
+ if not values:
81
+ print(message)
82
+ else:
83
+ values_str = ', '.join(str(x) for x in values)
84
+ print(f'{message}: {values_str}')
85
+
86
+ log('My numbers', 1, 2)
87
+ log('Hi')
88
+
89
+ # Pass a sequence as *args
90
+ favorites = [7, 33, 99]
91
+ log('Favorites', *favorites)
92
+ ```
93
+
94
+ **Caveats:**
95
+ - `*args` are converted to a tuple (memory issue with generators)
96
+ - Adding positional args before *args breaks callers if not careful
97
+ - Use keyword-only args after *args for new parameters
98
+
99
+ ## Item 23: Provide Optional Behavior with Keyword Arguments
100
+ ```python
101
+ def flow_rate(weight_diff, time_diff, *, period=1): # period is keyword-only
102
+ return (weight_diff / time_diff) * period
103
+
104
+ # Callers must use keyword
105
+ flow_rate(1, 2, period=3600)
106
+ flow_rate(1, 2, 3600) # TypeError!
107
+ ```
108
+
109
+ - Keyword args make function calls more readable
110
+ - They provide default values for optional behavior
111
+ - Can be added to existing functions without breaking callers
112
+
113
+ ## Item 24: Use None and Docstrings to Specify Dynamic Default Arguments
114
+ ```python
115
+ # BAD — mutable default is shared across calls!
116
+ def log(message, when=datetime.now()): # BUG: evaluated once at import
117
+ print(f'{when}: {message}')
118
+
119
+ # GOOD — use None sentinel
120
+ def log(message, when=None):
121
+ """Log a message with a timestamp.
122
+
123
+ Args:
124
+ message: Message to print.
125
+ when: datetime of when the message occurred.
126
+ Defaults to the present time.
127
+ """
128
+ if when is None:
129
+ when = datetime.now()
130
+ print(f'{when}: {message}')
131
+ ```
132
+
133
+ - **Never use mutable objects** as default argument values (lists, dicts, sets, datetime.now())
134
+ - Use `None` and document the actual default in the docstring
135
+ - This also applies to type hints: `when: Optional[datetime] = None`
136
+
137
+ ## Item 25: Enforce Clarity with Keyword-Only and Positional-Only Arguments
138
+ ```python
139
+ # Keyword-only: after * in signature
140
+ def safe_division(number, divisor, *,
141
+ ignore_overflow=False,
142
+ ignore_zero_division=False):
143
+ pass
144
+
145
+ # Positional-only: before / in signature (Python 3.8+)
146
+ def safe_division(numerator, denominator, /,
147
+ *, ignore_overflow=False):
148
+ pass
149
+
150
+ # Combined: positional-only / regular / keyword-only
151
+ def safe_division(numerator, denominator, /,
152
+ ndigits=10, *,
153
+ ignore_overflow=False):
154
+ pass
155
+ ```
156
+
157
+ - `/` separates positional-only from regular params
158
+ - `*` separates regular from keyword-only params
159
+ - Use positional-only for params where the name is an implementation detail
160
+ - Use keyword-only for boolean flags and optional configuration
161
+
162
+ ## Item 26: Define Function Decorators with functools.wraps
163
+ ```python
164
+ from functools import wraps
165
+
166
+ # BAD — decorator hides original function metadata
167
+ def trace(func):
168
+ def wrapper(*args, **kwargs):
169
+ result = func(*args, **kwargs)
170
+ print(f'{func.__name__}({args}, {kwargs}) -> {result}')
171
+ return result
172
+ return wrapper
173
+
174
+ # GOOD — preserves function metadata
175
+ def trace(func):
176
+ @wraps(func)
177
+ def wrapper(*args, **kwargs):
178
+ result = func(*args, **kwargs)
179
+ print(f'{func.__name__}({args}, {kwargs}) -> {result}')
180
+ return result
181
+ return wrapper
182
+ ```
183
+
184
+ - `@wraps` copies the inner function's metadata (__name__, __module__, __doc__)
185
+ - Without it, debugging tools, serializers, and `help()` break
186
+ - **Always** use `@wraps` on decorator wrapper functions