@booklib/skills 1.0.0 → 1.2.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.
@@ -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
@@ -0,0 +1,213 @@
1
+ # Chapter 7: Concurrency and Parallelism (Items 52-64)
2
+
3
+ ## Item 52: Use subprocess to Manage Child Processes
4
+ ```python
5
+ import subprocess
6
+
7
+ # Run a command and capture output
8
+ result = subprocess.run(
9
+ ['echo', 'Hello from subprocess'],
10
+ capture_output=True,
11
+ text=True
12
+ )
13
+ print(result.stdout)
14
+
15
+ # Set timeout
16
+ result = subprocess.run(
17
+ ['sleep', '10'],
18
+ timeout=5 # raises TimeoutExpired after 5 seconds
19
+ )
20
+
21
+ # Pipe data to child process
22
+ result = subprocess.run(
23
+ ['openssl', 'enc', '-aes-256-cbc', '-pass', 'pass:key'],
24
+ input=b'data to encrypt',
25
+ capture_output=True
26
+ )
27
+
28
+ # Run parallel child processes
29
+ procs = [subprocess.Popen(['cmd', arg]) for arg in args]
30
+ for proc in procs:
31
+ proc.communicate() # wait for each
32
+ ```
33
+
34
+ - Use `subprocess.run` for simple command execution
35
+ - Use `subprocess.Popen` for parallel or streaming processes
36
+ - Always set timeouts to prevent hanging
37
+
38
+ ## Item 53: Use Threads for Blocking I/O, Avoid for Parallelism
39
+ ```python
40
+ import threading
41
+
42
+ # Threads for I/O parallelism — GOOD
43
+ def download(url):
44
+ resp = urllib.request.urlopen(url)
45
+ return resp.read()
46
+
47
+ threads = [threading.Thread(target=download, args=(url,)) for url in urls]
48
+ for t in threads:
49
+ t.start()
50
+ for t in threads:
51
+ t.join()
52
+ ```
53
+
54
+ - The GIL prevents true CPU parallelism with threads
55
+ - Threads ARE useful for blocking I/O (network, file system, etc.)
56
+ - For CPU-bound work, use `multiprocessing` or `concurrent.futures.ProcessPoolExecutor`
57
+ - Never use threads for CPU-intensive computation
58
+
59
+ ## Item 54: Use Lock to Prevent Data Races in Threads
60
+ ```python
61
+ from threading import Lock
62
+
63
+ class Counter:
64
+ def __init__(self):
65
+ self.count = 0
66
+ self.lock = Lock()
67
+
68
+ def increment(self):
69
+ with self.lock: # context manager is cleanest
70
+ self.count += 1
71
+ ```
72
+
73
+ - The GIL does NOT prevent data races on Python objects
74
+ - Operations like `+=` are not atomic — they involve read + modify + write
75
+ - Always use `Lock` when multiple threads modify shared state
76
+ - Use `with lock:` context manager for clean acquire/release
77
+
78
+ ## Item 55: Use Queue to Coordinate Work Between Threads
79
+ ```python
80
+ from queue import Queue
81
+ from threading import Thread
82
+
83
+ def producer(queue):
84
+ for item in generate_items():
85
+ queue.put(item)
86
+ queue.put(None) # sentinel to signal done
87
+
88
+ def consumer(queue):
89
+ while True:
90
+ item = queue.get()
91
+ if item is None:
92
+ break
93
+ process(item)
94
+ queue.task_done()
95
+
96
+ queue = Queue(maxsize=10) # bounded for backpressure
97
+ Thread(target=producer, args=(queue,)).start()
98
+ Thread(target=consumer, args=(queue,)).start()
99
+ queue.join() # wait for all items to be processed
100
+ ```
101
+
102
+ - `Queue` provides thread-safe FIFO communication
103
+ - Use `maxsize` for backpressure (producer blocks when full)
104
+ - Use `task_done()` + `join()` for completion tracking
105
+ - Use sentinel values (None) to signal shutdown
106
+
107
+ ## Item 56: Know How to Recognize When Concurrency Is Necessary
108
+ - Concurrency is needed when you have fan-out (one task spawning many) and fan-in (collecting results)
109
+ - Signs you need concurrency: I/O-bound waits, independent tasks, pipeline processing
110
+ - Start simple (sequential), then add concurrency only when needed
111
+
112
+ ## Item 57: Avoid Creating New Thread Instances for On-demand Fan-out
113
+ - Creating a thread per task doesn't scale (thread creation overhead, memory)
114
+ - Use thread pools instead (Item 58/59)
115
+
116
+ ## Item 58: Understand How Using Queue for Concurrency Requires Refactoring
117
+ - Queue-based pipelines require significant refactoring
118
+ - Consider `concurrent.futures` for simpler patterns
119
+
120
+ ## Item 59: Consider ThreadPoolExecutor When Threads Are Necessary for Concurrency
121
+ ```python
122
+ from concurrent.futures import ThreadPoolExecutor
123
+
124
+ def fetch_url(url):
125
+ return urllib.request.urlopen(url).read()
126
+
127
+ with ThreadPoolExecutor(max_workers=5) as executor:
128
+ # Submit individual tasks
129
+ future = executor.submit(fetch_url, 'https://example.com')
130
+ result = future.result()
131
+
132
+ # Map over multiple inputs
133
+ results = list(executor.map(fetch_url, urls))
134
+ ```
135
+
136
+ - Simpler than manual thread + Queue management
137
+ - Automatically manages thread lifecycle
138
+ - `max_workers` controls parallelism
139
+ - Use `ProcessPoolExecutor` for CPU-bound tasks
140
+
141
+ ## Item 60: Achieve Highly Concurrent I/O with Coroutines
142
+ ```python
143
+ import asyncio
144
+
145
+ async def fetch_data(url):
146
+ # async I/O operation
147
+ reader, writer = await asyncio.open_connection(host, port)
148
+ writer.write(request)
149
+ data = await reader.read()
150
+ return data
151
+
152
+ async def main():
153
+ # Run multiple coroutines concurrently
154
+ results = await asyncio.gather(
155
+ fetch_data('url1'),
156
+ fetch_data('url2'),
157
+ fetch_data('url3'),
158
+ )
159
+
160
+ asyncio.run(main())
161
+ ```
162
+
163
+ - Coroutines enable thousands of concurrent I/O operations
164
+ - Use `async def` and `await` keywords
165
+ - `asyncio.gather` runs multiple coroutines concurrently
166
+ - Far more efficient than threads for I/O-heavy workloads
167
+
168
+ ## Item 61: Know How to Port Threaded I/O to asyncio
169
+ - Replace `threading.Thread` with `async def` coroutines
170
+ - Replace blocking I/O calls with `await async_version`
171
+ - Replace `Lock` with `asyncio.Lock`
172
+ - Replace `Queue` with `asyncio.Queue`
173
+ - Use `asyncio.run()` as the entry point
174
+
175
+ ## Item 62: Mix Threads and Coroutines to Ease the Transition to asyncio
176
+ ```python
177
+ # Run blocking code in a thread from async context
178
+ import asyncio
179
+
180
+ async def main():
181
+ loop = asyncio.get_event_loop()
182
+ result = await loop.run_in_executor(None, blocking_function, arg)
183
+
184
+ # Run async code from synchronous context
185
+ def sync_function():
186
+ loop = asyncio.new_event_loop()
187
+ result = loop.run_until_complete(async_function())
188
+ ```
189
+
190
+ - Use `run_in_executor` to call blocking code from async code
191
+ - Allows gradual migration from threads to asyncio
192
+ - Never call blocking functions directly in async code (it blocks the event loop)
193
+
194
+ ## Item 63: Avoid Blocking the asyncio Event Loop to Maximize Responsiveness
195
+ - Never use `time.sleep()` in async code — use `await asyncio.sleep()`
196
+ - Never do CPU-heavy work in coroutines — use `run_in_executor`
197
+ - Never use blocking I/O calls — use async equivalents (aiohttp, aiofiles, etc.)
198
+ - Profile with `asyncio.get_event_loop().slow_callback_duration`
199
+
200
+ ## Item 64: Consider concurrent.futures for True Parallelism
201
+ ```python
202
+ from concurrent.futures import ProcessPoolExecutor
203
+
204
+ def cpu_heavy(data):
205
+ return complex_computation(data)
206
+
207
+ with ProcessPoolExecutor() as executor:
208
+ results = list(executor.map(cpu_heavy, data_chunks))
209
+ ```
210
+
211
+ - `ProcessPoolExecutor` bypasses the GIL for true CPU parallelism
212
+ - Data is serialized between processes (use for independent tasks)
213
+ - Same API as `ThreadPoolExecutor` — easy to switch
@@ -0,0 +1,248 @@
1
+ # Chapter 8: Robustness and Performance (Items 65-76)
2
+
3
+ ## Item 65: Take Advantage of Each Block in try/except/else/finally
4
+ ```python
5
+ # Full structure
6
+ try:
7
+ # Code that might raise
8
+ result = dangerous_operation()
9
+ except SomeError as e:
10
+ # Handle specific error
11
+ log_error(e)
12
+ except (TypeError, ValueError):
13
+ # Handle multiple error types
14
+ handle_bad_input()
15
+ else:
16
+ # Runs ONLY if no exception was raised
17
+ # Use for code that depends on try succeeding
18
+ process(result)
19
+ finally:
20
+ # ALWAYS runs, even if exception was raised
21
+ # Use for cleanup (closing files, releasing locks)
22
+ cleanup()
23
+ ```
24
+
25
+ - `else` block: reduces code in `try`, makes it clear what you're protecting
26
+ - `finally` block: guaranteed cleanup
27
+ - Don't put too much in `try` — only the code that can raise the expected exception
28
+
29
+ ## Item 66: Consider contextlib and with Statements for Reusable try/finally Behavior
30
+ ```python
31
+ from contextlib import contextmanager
32
+
33
+ @contextmanager
34
+ def log_level(level, name):
35
+ logger = logging.getLogger(name)
36
+ old_level = logger.level
37
+ logger.setLevel(level)
38
+ try:
39
+ yield logger
40
+ finally:
41
+ logger.setLevel(old_level)
42
+
43
+ with log_level(logging.DEBUG, 'my-log') as logger:
44
+ logger.debug('Debug message')
45
+ # Level is automatically restored after the block
46
+ ```
47
+
48
+ - Use `contextlib.contextmanager` for simple context managers
49
+ - Use `with` statements instead of manual try/finally
50
+ - The `yield` in a context manager is where the `with` block executes
51
+
52
+ ## Item 67: Use datetime Instead of time for Local Clocks
53
+ ```python
54
+ from datetime import datetime, timezone
55
+ import pytz # or zoneinfo (Python 3.9+)
56
+
57
+ # BAD — time module is unreliable for timezones
58
+ import time
59
+ time.localtime() # platform-dependent behavior
60
+
61
+ # GOOD — datetime with explicit timezone
62
+ now = datetime.now(tz=timezone.utc)
63
+
64
+ # Convert between timezones
65
+ eastern = pytz.timezone('US/Eastern')
66
+ local_time = now.astimezone(eastern)
67
+
68
+ # Python 3.9+ — use zoneinfo
69
+ from zoneinfo import ZoneInfo
70
+ eastern = ZoneInfo('America/New_York')
71
+ local_time = now.astimezone(eastern)
72
+ ```
73
+
74
+ - Always store/transmit times in UTC
75
+ - Convert to local time only for display
76
+ - Use `pytz` or `zoneinfo` for timezone handling
77
+ - Never use the `time` module for timezone conversions
78
+
79
+ ## Item 68: Make pickle Reliable with copyreg
80
+ ```python
81
+ import copyreg
82
+ import pickle
83
+
84
+ class GameState:
85
+ def __init__(self, level=0, lives=4, points=0):
86
+ self.level = level
87
+ self.lives = lives
88
+ self.points = points
89
+
90
+ def pickle_game_state(game_state):
91
+ kwargs = game_state.__dict__
92
+ return unpickle_game_state, (kwargs,)
93
+
94
+ def unpickle_game_state(kwargs):
95
+ return GameState(**kwargs)
96
+
97
+ copyreg.pickle(GameState, pickle_game_state)
98
+ ```
99
+
100
+ - `copyreg` makes pickle forward-compatible when classes change
101
+ - Register custom serialization functions for your classes
102
+ - Always provide default values for new attributes
103
+
104
+ ## Item 69: Use decimal When Precision Matters
105
+ ```python
106
+ from decimal import Decimal, ROUND_UP
107
+
108
+ # BAD — float precision issues
109
+ rate = 1.45
110
+ seconds = 222
111
+ cost = rate * seconds / 60 # 5.364999999999999
112
+
113
+ # GOOD — Decimal for exact arithmetic
114
+ rate = Decimal('1.45')
115
+ seconds = Decimal('222')
116
+ cost = rate * seconds / Decimal('60')
117
+ rounded = cost.quantize(Decimal('0.01'), rounding=ROUND_UP)
118
+ ```
119
+
120
+ - Use `Decimal` for financial calculations, exact fractions
121
+ - Always construct from strings (`Decimal('1.45')`) not floats (`Decimal(1.45)`)
122
+ - Use `quantize` for rounding control
123
+
124
+ ## Item 70: Profile Before Optimizing
125
+ ```python
126
+ from cProfile import Profile
127
+ from pstats import Stats
128
+
129
+ profiler = Profile()
130
+ profiler.runcall(my_function, arg1, arg2)
131
+
132
+ stats = Stats(profiler)
133
+ stats.strip_dirs()
134
+ stats.sort_stats('cumulative')
135
+ stats.print_stats()
136
+ ```
137
+
138
+ - Never guess where bottlenecks are — profile first
139
+ - Use `cProfile` for C-extension speed profiling
140
+ - `cumulative` time shows total time including sub-calls
141
+ - `tottime` shows time in the function itself (excluding sub-calls)
142
+ - Focus optimization on the top functions by cumulative time
143
+
144
+ ## Item 71: Prefer deque for Producer-Consumer Queues
145
+ ```python
146
+ from collections import deque
147
+
148
+ # FIFO queue operations
149
+ queue = deque()
150
+ queue.append('item') # O(1) add to right
151
+ item = queue.popleft() # O(1) remove from left
152
+
153
+ # BAD — list as queue
154
+ queue = []
155
+ queue.append('item') # O(1)
156
+ item = queue.pop(0) # O(n)! shifts all elements
157
+ ```
158
+
159
+ - `list.pop(0)` is O(n); `deque.popleft()` is O(1)
160
+ - `deque` also supports `maxlen` for bounded buffers
161
+ - Use `deque` for any FIFO pattern
162
+
163
+ ## Item 72: Consider Searching Sorted Sequences with bisect
164
+ ```python
165
+ import bisect
166
+
167
+ sorted_list = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
168
+
169
+ # Find insertion point
170
+ index = bisect.bisect_left(sorted_list, 12) # 3
171
+ index = bisect.bisect_right(sorted_list, 12) # 4
172
+
173
+ # Insert while maintaining sort order
174
+ bisect.insort(sorted_list, 15) # inserts 15 in correct position
175
+ ```
176
+
177
+ - Binary search is O(log n) vs O(n) for linear search
178
+ - Use `bisect_left` for leftmost position, `bisect_right` for rightmost
179
+ - `insort` keeps list sorted after insertion
180
+ - Requires the sequence to already be sorted
181
+
182
+ ## Item 73: Know How to Use heapq for Priority Queues
183
+ ```python
184
+ import heapq
185
+
186
+ # Create a min-heap
187
+ heap = []
188
+ heapq.heappush(heap, 5)
189
+ heapq.heappush(heap, 1)
190
+ heapq.heappush(heap, 3)
191
+
192
+ # Pop smallest
193
+ smallest = heapq.heappop(heap) # 1
194
+
195
+ # Get n smallest/largest
196
+ heapq.nsmallest(3, data)
197
+ heapq.nlargest(3, data)
198
+
199
+ # Priority queue with tuples
200
+ heapq.heappush(heap, (priority, item))
201
+ ```
202
+
203
+ - heapq provides O(log n) push and pop operations
204
+ - Always a min-heap (smallest first)
205
+ - For max-heap, negate the values
206
+ - Use for priority queues, top-K problems, merge sorted streams
207
+
208
+ ## Item 74: Consider memoryview and bytearray for Zero-Copy Interactions with bytes
209
+ ```python
210
+ # BAD — copying bytes on every slice
211
+ data = b'large data...'
212
+ chunk = data[10:20] # creates a new bytes object
213
+
214
+ # GOOD — zero-copy with memoryview
215
+ data = bytearray(b'large data...')
216
+ view = memoryview(data)
217
+ chunk = view[10:20] # no copy, just a view
218
+ chunk[:5] = b'hello' # writes directly to original data
219
+ ```
220
+
221
+ - `memoryview` provides zero-copy slicing of bytes-like objects
222
+ - Essential for high-performance I/O and data processing
223
+ - Works with `bytearray`, `array.array`, NumPy arrays
224
+ - Use for socket I/O, file I/O, binary protocol parsing
225
+
226
+ ## Item 75: Use repr Strings for Debugging Output
227
+ ```python
228
+ class MyClass:
229
+ def __init__(self, value):
230
+ self.value = value
231
+
232
+ def __repr__(self):
233
+ return f'{self.__class__.__name__}({self.value!r})'
234
+
235
+ def __str__(self):
236
+ return f'MyClass with value {self.value}'
237
+ ```
238
+
239
+ - `repr()` gives an unambiguous string for debugging
240
+ - `str()` gives a human-readable string
241
+ - Always implement `__repr__` on your classes
242
+ - Use `!r` in f-strings for repr formatting: `f'{obj!r}'`
243
+
244
+ ## Item 76: Verify Related Behaviors in TestCase Subclasses
245
+ (Cross-reference with Chapter 9 Testing)
246
+ - Group related tests in TestCase subclasses
247
+ - Use descriptive test method names
248
+ - Test both success and failure cases