agentic-team-templates 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,251 @@
1
+ # Python Patterns and Idioms
2
+
3
+ Idiomatic Python leverages the language's strengths — duck typing, protocols, generators, decorators, and the data model. Write Pythonic code, not Java-in-Python.
4
+
5
+ ## Data Model (Dunder Methods)
6
+
7
+ ```python
8
+ from functools import total_ordering
9
+
10
+ @total_ordering
11
+ @dataclass(frozen=True, slots=True)
12
+ class Money:
13
+ amount: Decimal
14
+ currency: str
15
+
16
+ def __add__(self, other: Money) -> Money:
17
+ if self.currency != other.currency:
18
+ raise ValueError(f"Cannot add {self.currency} and {other.currency}")
19
+ return Money(self.amount + other.amount, self.currency)
20
+
21
+ def __eq__(self, other: object) -> bool:
22
+ if not isinstance(other, Money):
23
+ return NotImplemented
24
+ return self.amount == other.amount and self.currency == other.currency
25
+
26
+ def __lt__(self, other: Money) -> bool:
27
+ if self.currency != other.currency:
28
+ raise ValueError(f"Cannot compare {self.currency} and {other.currency}")
29
+ return self.amount < other.amount
30
+
31
+ def __str__(self) -> str:
32
+ return f"{self.currency} {self.amount:.2f}"
33
+
34
+ def __repr__(self) -> str:
35
+ return f"Money(amount={self.amount!r}, currency={self.currency!r})"
36
+
37
+ def __bool__(self) -> bool:
38
+ return self.amount != 0
39
+ ```
40
+
41
+ ## Generators and Iterators
42
+
43
+ ```python
44
+ from collections.abc import Iterator, Generator
45
+
46
+ # Generators for lazy evaluation — process one item at a time
47
+ def read_large_file(path: Path) -> Iterator[str]:
48
+ with open(path) as f:
49
+ for line in f:
50
+ yield line.strip()
51
+
52
+ # Generator expressions over list comprehensions when you iterate once
53
+ total = sum(order.total for order in orders) # No intermediate list
54
+
55
+ # Generator pipelines
56
+ def pipeline(path: Path) -> Iterator[Record]:
57
+ lines = read_large_file(path)
58
+ parsed = (parse_record(line) for line in lines)
59
+ valid = (record for record in parsed if record.is_valid())
60
+ yield from valid
61
+
62
+ # itertools for complex iteration
63
+ from itertools import chain, islice, groupby, batched
64
+
65
+ # Process in batches (3.12+)
66
+ for batch in batched(items, 100):
67
+ process_batch(batch)
68
+
69
+ # Chain multiple iterables
70
+ for item in chain(list_a, list_b, list_c):
71
+ process(item)
72
+ ```
73
+
74
+ ## Decorators
75
+
76
+ ```python
77
+ from functools import wraps
78
+ from typing import ParamSpec, TypeVar
79
+
80
+ P = ParamSpec("P")
81
+ R = TypeVar("R")
82
+
83
+ # Properly typed decorator that preserves signatures
84
+ def retry(max_attempts: int = 3, delay: float = 1.0):
85
+ def decorator(func: Callable[P, R]) -> Callable[P, R]:
86
+ @wraps(func)
87
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
88
+ last_error: Exception | None = None
89
+ for attempt in range(max_attempts):
90
+ try:
91
+ return func(*args, **kwargs)
92
+ except Exception as e:
93
+ last_error = e
94
+ if attempt < max_attempts - 1:
95
+ time.sleep(delay * (2 ** attempt))
96
+ raise last_error # type: ignore[misc]
97
+ return wrapper
98
+ return decorator
99
+
100
+ @retry(max_attempts=3, delay=0.5)
101
+ def call_api(url: str) -> Response:
102
+ ...
103
+
104
+ # Class-based decorator for complex state
105
+ class CachedProperty:
106
+ """Descriptor that caches the result of a method call."""
107
+ def __init__(self, func: Callable[..., Any]) -> None:
108
+ self.func = func
109
+ self.attrname: str | None = None
110
+
111
+ def __set_name__(self, owner: type, name: str) -> None:
112
+ self.attrname = name
113
+
114
+ def __get__(self, obj: Any, objtype: type | None = None) -> Any:
115
+ if obj is None:
116
+ return self
117
+ assert self.attrname is not None
118
+ cache = obj.__dict__
119
+ if self.attrname not in cache:
120
+ cache[self.attrname] = self.func(obj)
121
+ return cache[self.attrname]
122
+
123
+ # Or just use functools.cached_property — it's built in
124
+ ```
125
+
126
+ ## Context Managers
127
+
128
+ ```python
129
+ from contextlib import contextmanager, asynccontextmanager
130
+
131
+ # Synchronous context manager
132
+ @contextmanager
133
+ def timed_operation(name: str) -> Iterator[None]:
134
+ start = time.monotonic()
135
+ try:
136
+ yield
137
+ finally:
138
+ elapsed = time.monotonic() - start
139
+ logger.info(f"{name} took {elapsed:.3f}s")
140
+
141
+ with timed_operation("database query"):
142
+ results = db.execute(query)
143
+
144
+ # Async context manager
145
+ @asynccontextmanager
146
+ async def managed_connection(url: str) -> AsyncIterator[Connection]:
147
+ conn = await connect(url)
148
+ try:
149
+ yield conn
150
+ finally:
151
+ await conn.close()
152
+
153
+ # contextlib.suppress for intentionally ignored exceptions
154
+ from contextlib import suppress
155
+
156
+ with suppress(FileNotFoundError):
157
+ os.remove(temp_file)
158
+
159
+ # ExitStack for dynamic resource management
160
+ from contextlib import ExitStack
161
+
162
+ with ExitStack() as stack:
163
+ files = [stack.enter_context(open(f)) for f in file_paths]
164
+ process_all(files)
165
+ ```
166
+
167
+ ## Descriptors
168
+
169
+ ```python
170
+ # Descriptors power properties, classmethods, staticmethods
171
+ class Validated:
172
+ """Descriptor that validates on assignment."""
173
+ def __init__(self, validator: Callable[[Any], bool], message: str) -> None:
174
+ self.validator = validator
175
+ self.message = message
176
+ self.attr_name = ""
177
+
178
+ def __set_name__(self, owner: type, name: str) -> None:
179
+ self.attr_name = name
180
+
181
+ def __set__(self, obj: Any, value: Any) -> None:
182
+ if not self.validator(value):
183
+ raise ValueError(f"{self.attr_name}: {self.message}")
184
+ obj.__dict__[self.attr_name] = value
185
+
186
+ def __get__(self, obj: Any, objtype: type | None = None) -> Any:
187
+ if obj is None:
188
+ return self
189
+ return obj.__dict__.get(self.attr_name)
190
+
191
+ class User:
192
+ name = Validated(lambda v: isinstance(v, str) and len(v) > 0, "must be non-empty string")
193
+ age = Validated(lambda v: isinstance(v, int) and 0 <= v <= 150, "must be 0-150")
194
+ ```
195
+
196
+ ## collections Module
197
+
198
+ ```python
199
+ from collections import defaultdict, Counter, deque, OrderedDict
200
+
201
+ # defaultdict — avoid KeyError boilerplate
202
+ word_counts: defaultdict[str, int] = defaultdict(int)
203
+ for word in words:
204
+ word_counts[word] += 1
205
+
206
+ # Counter — counting and most common
207
+ counter = Counter(words)
208
+ top_10 = counter.most_common(10)
209
+
210
+ # deque — O(1) append/pop from both ends
211
+ recent: deque[Event] = deque(maxlen=100)
212
+ recent.append(event) # Oldest auto-evicted when full
213
+
214
+ # Named tuples for lightweight immutable records (prefer dataclasses for new code)
215
+ from typing import NamedTuple
216
+
217
+ class Point(NamedTuple):
218
+ x: float
219
+ y: float
220
+ ```
221
+
222
+ ## Anti-Patterns
223
+
224
+ ```python
225
+ # Never: Mutable default arguments
226
+ def append(item, target=[]): # Shared across all calls!
227
+ target.append(item)
228
+ return target
229
+
230
+ # Never: Bare except
231
+ try:
232
+ risky()
233
+ except: # Catches KeyboardInterrupt, SystemExit — everything
234
+ pass
235
+
236
+ # Never: String formatting with % or .format() for f-string-eligible code
237
+ name = "world"
238
+ greeting = f"hello, {name}" # Not "hello, %s" % name
239
+
240
+ # Never: Using type() for type checks
241
+ if type(x) == int: # Fails for subclasses
242
+ ...
243
+ if isinstance(x, int): # Correct
244
+ ...
245
+
246
+ # Never: Global mutable state
247
+ _cache = {} # Module-level mutable dict — test nightmare
248
+
249
+ # Never: Star imports
250
+ from os.path import * # Pollutes namespace, hides origins
251
+ ```
@@ -0,0 +1,208 @@
1
+ # Python Performance
2
+
3
+ Python is not the fastest language. It doesn't need to be. Know where the bottlenecks are, profile before optimizing, and reach for the right tool when raw speed matters.
4
+
5
+ ## Profile First
6
+
7
+ ```python
8
+ # cProfile for function-level profiling
9
+ import cProfile
10
+ cProfile.run("main()", sort="cumulative")
11
+
12
+ # line_profiler for line-by-line analysis
13
+ # @profile decorator (after pip install line-profiler)
14
+ # kernprof -l -v script.py
15
+
16
+ # memory_profiler for memory usage
17
+ # @profile decorator (after pip install memory-profiler)
18
+ # python -m memory_profiler script.py
19
+
20
+ # py-spy for production profiling without code changes
21
+ # py-spy record -o profile.svg -- python myapp.py
22
+
23
+ # timeit for micro-benchmarks
24
+ import timeit
25
+ timeit.timeit("sorted(data)", globals={"data": list(range(1000))}, number=10000)
26
+ ```
27
+
28
+ ## Data Structures
29
+
30
+ ```python
31
+ # Choose the right data structure for the access pattern
32
+
33
+ # dict — O(1) lookup, insertion, deletion
34
+ # Use when: key-value mapping with frequent lookups
35
+ cache: dict[str, Result] = {}
36
+
37
+ # set — O(1) membership testing
38
+ # Use when: uniqueness checks, set operations
39
+ seen: set[str] = set()
40
+ if item_id not in seen:
41
+ process(item)
42
+ seen.add(item_id)
43
+
44
+ # deque — O(1) append/pop from both ends
45
+ # Use when: queue, sliding window, bounded history
46
+ from collections import deque
47
+ recent_events: deque[Event] = deque(maxlen=1000)
48
+
49
+ # heapq — O(log n) push/pop for priority queue
50
+ # Use when: top-N, scheduling, priority processing
51
+ import heapq
52
+ top_10 = heapq.nlargest(10, items, key=lambda x: x.score)
53
+
54
+ # bisect — O(log n) search in sorted lists
55
+ # Use when: maintaining sorted order with insertions
56
+ import bisect
57
+ bisect.insort(sorted_list, new_item)
58
+ ```
59
+
60
+ ## Avoiding Common Bottlenecks
61
+
62
+ ```python
63
+ # String concatenation — use join, not +=
64
+ # Bad: O(n²) — creates new string each iteration
65
+ result = ""
66
+ for chunk in chunks:
67
+ result += chunk
68
+
69
+ # Good: O(n)
70
+ result = "".join(chunks)
71
+
72
+ # List comprehensions over loops for simple transforms
73
+ # Good: Faster due to C-level optimization
74
+ squares = [x * x for x in range(1000)]
75
+
76
+ # Generator expressions when you don't need the full list
77
+ total = sum(x * x for x in range(1_000_000)) # No intermediate list
78
+
79
+ # dict.get() over try/except for missing keys (in tight loops)
80
+ value = mapping.get(key, default)
81
+
82
+ # Local variable lookup is faster than global/attribute
83
+ # In hot loops, alias frequently accessed attributes
84
+ append = result.append # Local lookup in loop body
85
+ for item in items:
86
+ append(transform(item))
87
+ ```
88
+
89
+ ## Concurrency Models
90
+
91
+ ```python
92
+ # I/O-bound: asyncio or threading
93
+ # CPU-bound: multiprocessing or native extensions
94
+
95
+ # Threading for I/O parallelism (GIL is released during I/O)
96
+ from concurrent.futures import ThreadPoolExecutor
97
+
98
+ with ThreadPoolExecutor(max_workers=10) as executor:
99
+ results = list(executor.map(fetch_url, urls))
100
+
101
+ # Multiprocessing for CPU parallelism (bypasses GIL)
102
+ from concurrent.futures import ProcessPoolExecutor
103
+
104
+ with ProcessPoolExecutor() as executor:
105
+ results = list(executor.map(cpu_intensive_work, data_chunks))
106
+
107
+ # asyncio for high-concurrency I/O (thousands of connections)
108
+ # See async-python.md for patterns
109
+ ```
110
+
111
+ ## Caching
112
+
113
+ ```python
114
+ from functools import lru_cache, cache
115
+
116
+ # lru_cache for expensive pure function calls
117
+ @lru_cache(maxsize=256)
118
+ def fibonacci(n: int) -> int:
119
+ if n < 2:
120
+ return n
121
+ return fibonacci(n - 1) + fibonacci(n - 2)
122
+
123
+ # cache (3.9+) — unlimited cache, simpler API
124
+ @cache
125
+ def load_schema(name: str) -> Schema:
126
+ return Schema.from_file(f"schemas/{name}.json")
127
+
128
+ # Manual caching with TTL
129
+ from time import monotonic
130
+
131
+ class TTLCache:
132
+ def __init__(self, ttl: float) -> None:
133
+ self._cache: dict[str, tuple[float, Any]] = {}
134
+ self._ttl = ttl
135
+
136
+ def get(self, key: str) -> Any | None:
137
+ if key in self._cache:
138
+ expiry, value = self._cache[key]
139
+ if monotonic() < expiry:
140
+ return value
141
+ del self._cache[key]
142
+ return None
143
+
144
+ def set(self, key: str, value: Any) -> None:
145
+ self._cache[key] = (monotonic() + self._ttl, value)
146
+ ```
147
+
148
+ ## Slots and Memory
149
+
150
+ ```python
151
+ # __slots__ reduces memory per instance and speeds up attribute access
152
+ @dataclass(slots=True)
153
+ class Point:
154
+ x: float
155
+ y: float
156
+
157
+ # Without slots: ~200 bytes per instance (dict overhead)
158
+ # With slots: ~56 bytes per instance
159
+ # Matters when you have millions of instances
160
+
161
+ # sys.getsizeof for memory inspection
162
+ import sys
163
+ sys.getsizeof(my_object)
164
+
165
+ # tracemalloc for tracking allocations
166
+ import tracemalloc
167
+ tracemalloc.start()
168
+ # ... run code ...
169
+ snapshot = tracemalloc.take_snapshot()
170
+ for stat in snapshot.statistics("lineno")[:10]:
171
+ print(stat)
172
+ ```
173
+
174
+ ## When Python Isn't Fast Enough
175
+
176
+ ```python
177
+ # 1. NumPy/Pandas for vectorized numeric operations
178
+ import numpy as np
179
+ result = np.sum(array * weights) # C-level loop, orders of magnitude faster
180
+
181
+ # 2. Polars for DataFrames (faster than Pandas, Rust backend)
182
+ import polars as pl
183
+ df.filter(pl.col("age") > 30).group_by("city").agg(pl.col("salary").mean())
184
+
185
+ # 3. Cython or mypyc for compiling Python to C
186
+ # 4. PyO3/maturin for writing critical paths in Rust
187
+ # 5. ctypes/cffi for calling existing C libraries
188
+ ```
189
+
190
+ ## Anti-Patterns
191
+
192
+ ```python
193
+ # Never: Premature optimization
194
+ # Profile first. The bottleneck is almost never where you think.
195
+
196
+ # Never: Optimizing readability away
197
+ # A 10% speedup that makes code unmaintainable is a net loss.
198
+
199
+ # Never: Global imports of large modules in hot paths
200
+ def process():
201
+ import pandas as pd # Import overhead on every call
202
+ # Import at module level instead
203
+
204
+ # Never: Creating regex objects in loops
205
+ for line in lines:
206
+ match = re.search(r"pattern", line) # Recompiles every iteration
207
+ # Compile once: pattern = re.compile(r"pattern")
208
+ ```
@@ -0,0 +1,238 @@
1
+ # Python Testing
2
+
3
+ pytest is the standard. Tests are not optional. Every behavior has a test. Every bug fix starts with a failing test.
4
+
5
+ ## Fundamentals
6
+
7
+ ### Test Structure
8
+
9
+ ```python
10
+ # Arrange-Act-Assert
11
+ def test_user_creation() -> None:
12
+ # Arrange
13
+ repo = FakeUserRepo()
14
+ service = UserService(repo)
15
+ input_data = CreateUserInput(name="Alice", email="alice@example.com")
16
+
17
+ # Act
18
+ user = service.create(input_data)
19
+
20
+ # Assert
21
+ assert user.name == "Alice"
22
+ assert user.email == "alice@example.com"
23
+ assert user.id is not None
24
+ ```
25
+
26
+ ### Naming
27
+
28
+ ```python
29
+ # Test names describe the scenario and expected outcome
30
+ def test_create_user_with_valid_input_returns_user() -> None: ...
31
+ def test_create_user_with_duplicate_email_raises_conflict() -> None: ...
32
+ def test_create_user_with_empty_name_raises_validation_error() -> None: ...
33
+
34
+ # Not:
35
+ def test_create() -> None: ... # Create what? What scenario?
36
+ def test_1() -> None: ... # Meaningless
37
+ ```
38
+
39
+ ## Fixtures
40
+
41
+ ```python
42
+ import pytest
43
+
44
+ @pytest.fixture
45
+ def user_repo() -> FakeUserRepo:
46
+ return FakeUserRepo()
47
+
48
+ @pytest.fixture
49
+ def user_service(user_repo: FakeUserRepo) -> UserService:
50
+ return UserService(user_repo)
51
+
52
+ @pytest.fixture
53
+ def sample_user() -> User:
54
+ return User(id="123", name="Alice", email="alice@example.com")
55
+
56
+ # Fixtures with cleanup
57
+ @pytest.fixture
58
+ def temp_db():
59
+ db = create_test_database()
60
+ yield db
61
+ db.drop()
62
+
63
+ # Session-scoped fixtures for expensive setup
64
+ @pytest.fixture(scope="session")
65
+ def docker_postgres():
66
+ container = start_postgres_container()
67
+ yield container.connection_string
68
+ container.stop()
69
+ ```
70
+
71
+ ## Parametrize
72
+
73
+ ```python
74
+ @pytest.mark.parametrize(
75
+ "input_str, expected",
76
+ [
77
+ ("hello world", "hello-world"),
78
+ (" spaces ", "spaces"),
79
+ ("UPPER CASE", "upper-case"),
80
+ ("special!@#chars", "specialchars"),
81
+ ("already-slugified", "already-slugified"),
82
+ ("", ""),
83
+ ],
84
+ )
85
+ def test_slugify(input_str: str, expected: str) -> None:
86
+ assert slugify(input_str) == expected
87
+
88
+ # Parametrize with IDs for clear test output
89
+ @pytest.mark.parametrize(
90
+ "status_code, expected_error",
91
+ [
92
+ pytest.param(400, ValidationError, id="bad-request"),
93
+ pytest.param(404, NotFoundError, id="not-found"),
94
+ pytest.param(500, ServerError, id="server-error"),
95
+ ],
96
+ )
97
+ def test_error_mapping(status_code: int, expected_error: type[Exception]) -> None:
98
+ with pytest.raises(expected_error):
99
+ handle_response(MockResponse(status_code))
100
+ ```
101
+
102
+ ## Testing Exceptions
103
+
104
+ ```python
105
+ def test_division_by_zero_raises_value_error() -> None:
106
+ with pytest.raises(ValueError, match="divisor must be non-zero"):
107
+ divide(10, 0)
108
+
109
+ def test_missing_config_key_raises_key_error() -> None:
110
+ config = Config({})
111
+ with pytest.raises(KeyError) as exc_info:
112
+ config.get_required("missing_key")
113
+ assert "missing_key" in str(exc_info.value)
114
+ ```
115
+
116
+ ## Mocking
117
+
118
+ ```python
119
+ from unittest.mock import Mock, AsyncMock, patch, MagicMock
120
+
121
+ # Prefer dependency injection over patching
122
+ # Good: Inject the dependency
123
+ class UserService:
124
+ def __init__(self, repo: UserRepository, notifier: Notifier) -> None:
125
+ self.repo = repo
126
+ self.notifier = notifier
127
+
128
+ def test_create_user_sends_notification() -> None:
129
+ repo = FakeUserRepo()
130
+ notifier = Mock(spec=Notifier)
131
+
132
+ service = UserService(repo, notifier)
133
+ service.create(CreateUserInput(name="Alice", email="a@b.com"))
134
+
135
+ notifier.send_welcome.assert_called_once_with("a@b.com")
136
+
137
+ # When you must patch (third-party code, module-level functions)
138
+ @patch("mypackage.services.user_service.send_email")
139
+ def test_sends_email_on_signup(mock_send: Mock) -> None:
140
+ service = UserService(FakeUserRepo())
141
+ service.signup(email="test@example.com")
142
+ mock_send.assert_called_once_with(to="test@example.com", template="welcome")
143
+
144
+ # Async mocking
145
+ async def test_async_fetch() -> None:
146
+ client = AsyncMock(spec=HttpClient)
147
+ client.get.return_value = Response(status=200, body=b'{"ok": true}')
148
+
149
+ result = await fetch_data(client, "/api/data")
150
+ assert result == {"ok": True}
151
+ ```
152
+
153
+ ## Integration Testing
154
+
155
+ ```python
156
+ import pytest
157
+ from httpx import AsyncClient
158
+
159
+ @pytest.fixture
160
+ async def app():
161
+ """Create test application with test database."""
162
+ test_app = create_app(testing=True)
163
+ async with test_app.lifespan():
164
+ yield test_app
165
+
166
+ @pytest.fixture
167
+ async def client(app) -> AsyncClient:
168
+ async with AsyncClient(app=app, base_url="http://test") as ac:
169
+ yield ac
170
+
171
+ @pytest.mark.asyncio
172
+ async def test_create_and_retrieve_user(client: AsyncClient) -> None:
173
+ # Create
174
+ response = await client.post("/users", json={"name": "Alice", "email": "a@b.com"})
175
+ assert response.status_code == 201
176
+ user_id = response.json()["id"]
177
+
178
+ # Retrieve
179
+ response = await client.get(f"/users/{user_id}")
180
+ assert response.status_code == 200
181
+ assert response.json()["name"] == "Alice"
182
+ ```
183
+
184
+ ## Snapshot Testing
185
+
186
+ ```python
187
+ # Using syrupy or inline-snapshot
188
+ def test_api_response_format(snapshot) -> None:
189
+ response = generate_api_response(sample_data)
190
+ assert response == snapshot
191
+ ```
192
+
193
+ ## conftest.py Patterns
194
+
195
+ ```python
196
+ # tests/conftest.py — shared across all tests
197
+ import pytest
198
+
199
+ @pytest.fixture(autouse=True)
200
+ def _reset_singletons():
201
+ """Reset module-level singletons between tests."""
202
+ yield
203
+ SingletonClass._instance = None
204
+
205
+ @pytest.fixture
206
+ def freeze_time():
207
+ """Fixture that freezes time for deterministic tests."""
208
+ from freezegun import freeze_time
209
+ with freeze_time("2025-01-01T12:00:00Z") as frozen:
210
+ yield frozen
211
+ ```
212
+
213
+ ## Anti-Patterns
214
+
215
+ ```python
216
+ # Never: Tests that depend on execution order
217
+ # Each test must be independently runnable
218
+
219
+ # Never: Assertions without messages in complex tests
220
+ assert result # What is result? What did we expect?
221
+ # Better:
222
+ assert result.status == "active", f"Expected active, got {result.status}"
223
+
224
+ # Never: Testing implementation details
225
+ assert service._internal_cache == {"key": "value"} # Private API!
226
+ # Test through the public interface
227
+
228
+ # Never: Excessive mocking
229
+ # If you're mocking 5+ dependencies, the unit is too large — refactor
230
+
231
+ # Never: time.sleep() for synchronization in tests
232
+ time.sleep(1) # Slow AND flaky
233
+ # Use polling, events, or mock the clock
234
+
235
+ # Never: Tests without assertions
236
+ def test_user_creation() -> None:
237
+ service.create_user(data) # What are we verifying? Nothing.
238
+ ```