Stdlib-only recipes that come up in every Python codebase, written so they are safe on error and correct under the protocol rules in protocol-reference.md. AI-specific recipes are in ai-engineering-recipes.md.
Contents
- Files and file-like objects
- Temporary files and directories
- Working directory (pre-3.11 chdir)
- Locks and thread safety
- Thread and process pools
- Subprocesses
- Sockets and HTTP sessions
- Attribute, dict and global-state patching
- Logging level and capture
- Warnings, decimal precision, numpy error state
- Signals and timeouts (sync)
- Timing
- Resource pools (checkout/checkin)
- Indentation / nesting printer
Files and file-like objects
with open("prompts.jsonl", encoding="utf-8") as f: # always pass encoding for text
prompts = [json.loads(line) for line in f]
from pathlib import Path
text = Path("system_prompt.txt").read_text(encoding="utf-8") # opens + closes for you
with open("data.bin", "rb") as src, open("data.bak", "wb") as dst:
shutil.copyfileobj(src, dst)| Leaky | Safe |
|---|---|
data = open(p).read() | data = Path(p).read_text(encoding="utf-8") |
json.load(open(p)) | with open(p) as f: json.load(f) |
f = open(p); ...; f.close() | with open(p) as f: ... |
csv.reader(open(p)) | with open(p, newline="") as f: rows = list(csv.reader(f)) |
yaml.safe_load(open(p)) | with open(p) as f: yaml.safe_load(f) |
open(p).read() closes promptly on CPython (refcounting) but emits
ResourceWarning under -X dev, leaks on PyPy, and leaks if an exception
traceback keeps the frame alive. Treat it as a bug in library code.
Compressed and archive formats are all context managers:
import gzip, tarfile, zipfile
with gzip.open("corpus.jsonl.gz", "rt", encoding="utf-8") as f:
for line in f:
...
with zipfile.ZipFile("dataset.zip") as zf, zf.open("train.jsonl") as member:
first = member.readline()
with tarfile.open("model.tar.gz", "r:gz") as tf:
tf.extractall("weights/", filter="data") # filter= on 3.12+ (and backports); blocks path traversalTemporary files and directories
| API | Context manager? | Deleted when |
|---|---|---|
tempfile.TemporaryFile() | Yes | Closed (may never have a visible name) |
tempfile.NamedTemporaryFile(delete=True) | Yes | Closed. On Windows cannot be reopened while open; 3.12+ delete_on_close=False keeps it until the with exits |
tempfile.SpooledTemporaryFile(max_size=...) | Yes | Closed; in memory until max_size |
tempfile.TemporaryDirectory() | Yes (yields the path str) | with exits / .cleanup() |
tempfile.mkstemp() | No — returns (fd, path) | Never; you must os.close(fd) and os.remove(path) |
tempfile.mkdtemp() | No — returns path | Never; you must shutil.rmtree(path) |
import os
import tempfile
# Hand a real file path to a library that insists on a filename (e.g. an audio
# transcription SDK), cross-platform:
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "clip.wav")
with open(path, "wb") as f:
f.write(audio_bytes)
text = transcribe(path)
# 3.12+: NamedTemporaryFile that can be reopened by name on Windows too
with tempfile.NamedTemporaryFile("wb", suffix=".wav", delete_on_close=False) as f:
f.write(audio_bytes)
f.close()
text = transcribe(f.name)Working directory (pre-3.11 chdir)
ctx_tool.py scaffold --recipe cwd prints this, typed:
import os
from contextlib import contextmanager
from typing import Iterator
@contextmanager
def working_directory(path: str) -> Iterator[str]:
previous = os.getcwd()
os.chdir(path)
try:
yield path
finally:
os.chdir(previous)On 3.11+ use contextlib.chdir. Neither is thread-safe.
Locks and thread safety
import threading
_index_lock = threading.Lock() # module-level: shared by every caller
def add_to_index(index, vectors, ids) -> None:
with _index_lock: # acquire; release on any exit
index.add_with_ids(vectors, ids)| Primitive | Re-entrant | Notes |
|---|---|---|
threading.Lock | No | Nested acquire in the same thread deadlocks |
threading.RLock | Yes | Same thread may re-acquire; must release as many times |
threading.Semaphore(n) | — | n concurrent holders |
threading.BoundedSemaphore(n) | — | Raises ValueError on over-release |
threading.Condition(lock) | per lock | with cond: cond.wait_for(pred) |
Timeouts — the with form cannot take a timeout, so use explicit acquire with
a correct try/finally:
if not _index_lock.acquire(timeout=5):
raise TimeoutError("index busy")
try:
index.add(vectors)
finally:
_index_lock.release()Or wrap it:
from contextlib import contextmanager
from typing import Iterator
@contextmanager
def acquired(lock: threading.Lock, timeout: float) -> Iterator[None]:
if not lock.acquire(timeout=timeout):
raise TimeoutError(f"could not acquire lock within {timeout}s")
try:
yield
finally:
lock.release()Acquire before try: if acquire is inside the try and raises, finally
releases a lock you do not hold (RuntimeError: release unlocked lock) — or
worse, one another thread holds.
Is my context manager thread-safe? Checklist:
- State stored on
selfin__enter__is shared if the same instance is used from several threads (common withContextDecoratorinstances used as decorators). Store per-entry state in a local (generator CMs) orthreading.local()/contextvars.ContextVar. - Process-global effects (
os.environ,os.chdir,sys.stdout,signal,logginglevels,warningsfilters,numpy.seterr,decimalcontext is thread-local) are not thread-safe to override. - On free-threaded builds (3.13t) races that the GIL used to hide become real; guard shared state with a lock.
Thread and process pools
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=16) as pool: # exit = shutdown(wait=True)
futures = {pool.submit(fetch_page, url): url for url in urls}
for fut in as_completed(futures):
pages[futures[fut]] = fut.result()- Exiting the
withwaits for all submitted work. To abandon queued work on error, callpool.shutdown(wait=False, cancel_futures=True)(3.9+) in anexceptblock before re-raising. multiprocessing.Pool.__exit__callsterminate(), notclose()+join(): results not yet collected are lost. Collect results inside thewith.
from multiprocessing import Pool
with Pool(8) as pool:
chunks = pool.map(chunk_document, documents) # map blocks until done: safeSubprocesses
import subprocess
result = subprocess.run(["git", "status"], capture_output=True, text=True, timeout=30, check=True)subprocess.run manages the process for you. Use Popen only for streaming,
and then as a context manager (waits and closes pipes on exit):
with subprocess.Popen(
["python", "-u", "train.py"], stdout=subprocess.PIPE, text=True, bufsize=1,
) as proc:
try:
for line in proc.stdout:
log_metric_line(line)
except BaseException:
proc.kill() # don't wait forever for a child we are abandoning
raise
if proc.returncode:
raise subprocess.CalledProcessError(proc.returncode, proc.args)Sockets and HTTP sessions
import socket
with socket.create_connection(("localhost", 6379), timeout=2) as sock:
sock.sendall(b"PING\r\n")
reply = sock.recv(64)
import requests
with requests.Session() as s: # reuse TCP/TLS connections
s.headers["Authorization"] = f"Bearer {token}"
pages = [s.get(u, timeout=10).json() for u in urls]
with requests.get(url, stream=True, timeout=30) as r: # streamed body must be released
r.raise_for_status()
with open(dest, "wb") as f:
for block in r.iter_content(1 << 20):
f.write(block)
from urllib.request import urlopen
with urlopen("https://example.com/model-card.json", timeout=10) as resp:
card = json.load(resp)Attribute, dict and global-state patching
from unittest import mock
with mock.patch("app.llm.client.complete", return_value="stubbed answer") as fake:
assert answer("hi") == "stubbed answer"
fake.assert_called_once()
with mock.patch.object(settings, "max_tokens", 16):
...
with mock.patch.dict(PROMPTS, {"system": "You are terse."}):
...Patch where the name is looked up, not where it is defined
(app.service.httpx, not httpx).
Logging level and capture
import logging
from contextlib import contextmanager
from typing import Iterator, List
@contextmanager
def log_level(logger_name: str, level: int) -> Iterator[logging.Logger]:
logger = logging.getLogger(logger_name)
old = logger.level
logger.setLevel(level)
try:
yield logger
finally:
logger.setLevel(old)
@contextmanager
def captured_logs(logger_name: str = "", level: int = logging.INFO) -> Iterator[List[logging.LogRecord]]:
records: List[logging.LogRecord] = []
class _ListHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
records.append(record)
handler = _ListHandler(level)
logger = logging.getLogger(logger_name)
logger.addHandler(handler)
try:
yield records
finally:
logger.removeHandler(handler)
with log_level("httpx", logging.WARNING): # silence per-request INFO lines
run_batch()Warnings, decimal precision, numpy error state
import warnings
with warnings.catch_warnings(): # restores filters on exit (not thread-safe)
warnings.simplefilter("ignore", category=FutureWarning)
tokenizer = load_tokenizer()
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
legacy_call()
assert any(issubclass(w.category, DeprecationWarning) for w in caught)
from decimal import Decimal, localcontext
with localcontext() as ctx: # thread-local, safe
ctx.prec = 6
cost = Decimal(tokens) * Decimal("0.0000006")
import numpy as np
with np.errstate(divide="ignore", invalid="ignore"):
cosine = dot / (norm_a * norm_b) # zero vectors -> nan, no warning3.11+: warnings.catch_warnings(action="ignore", category=FutureWarning) does
both steps.
Signals and timeouts (sync)
Timeout a blocking call in the main thread (POSIX only; not in threads):
import signal
from contextlib import contextmanager
from typing import Iterator
@contextmanager
def time_limit(seconds: float) -> Iterator[None]:
def _raise(signum, frame):
raise TimeoutError(f"timed out after {seconds}s")
previous = signal.signal(signal.SIGALRM, _raise)
signal.setitimer(signal.ITIMER_REAL, seconds)
try:
yield
finally:
signal.setitimer(signal.ITIMER_REAL, 0)
signal.signal(signal.SIGALRM, previous)For portable timeouts on untrusted code (agent tool execution) run it in a
subprocess with timeout=; signals cannot interrupt C extensions that hold
the GIL.
Timing
ctx_tool.py scaffold --recipe timer prints a class that works as both a
with block and a decorator. Minimal generator version:
import time
from contextlib import contextmanager
from typing import Dict, Iterator
@contextmanager
def timed(label: str, sink: Dict[str, float]) -> Iterator[None]:
t0 = time.perf_counter()
try:
yield
finally:
sink[label] = sink.get(label, 0.0) + time.perf_counter() - t0
timings: Dict[str, float] = {}
with timed("chunk", timings):
chunks = chunker.split(doc)
with timed("embed", timings):
vecs = embedder.encode(chunks)Resource pools (checkout/checkin)
import queue
from contextlib import contextmanager
from typing import Callable, Generic, Iterator, TypeVar
T = TypeVar("T")
class Pool(Generic[T]):
"""Fixed-size pool of expensive objects (e.g. tokenizers, model replicas)."""
def __init__(self, factory: Callable[[], T], size: int) -> None:
self._items: "queue.Queue[T]" = queue.Queue()
for _ in range(size):
self._items.put(factory())
@contextmanager
def checkout(self, timeout: float = 30.0) -> Iterator[T]:
item = self._items.get(timeout=timeout) # raises queue.Empty on timeout
try:
yield item
finally:
self._items.put(item) # always returned, even on error
tokenizers = Pool(lambda: load_tokenizer("bpe.json"), size=4)
with tokenizers.checkout() as tok:
ids = tok.encode(text)If an error may leave the item in a bad state (a broken DB connection), replace
it instead of returning it: catch in an except, put(factory()), re-raise.
Indentation / nesting printer
A small example of state that must be restored on every exit path:
from contextlib import contextmanager
from typing import Iterator
class TreePrinter:
def __init__(self) -> None:
self.level = 0
@contextmanager
def indented(self) -> Iterator[None]:
self.level += 1
try:
yield
finally:
self.level -= 1
def line(self, text: str) -> None:
print(" " * self.level + text)
p = TreePrinter()
p.line("agent")
with p.indented():
p.line("tool: search")
with p.indented():
p.line("3 results")
p.line("done")python-beginner-concept-tutor
Teach Python concepts to beginners heading into AI engineering (data scientists, analysts, students, career switchers): plain-language definitions, analogies, minimal examples, line-by-line walkthroughs, common mistakes, check-your-understanding questions and mini exercises; Socratic hints before solutions; explaining tracebacks; mutable vs immutable, references and aliasing, is vs ==, integer division, floating point, slicing and off-by-one, indentation errors; a staged learning path from variables to calling an LLM API, embeddings and a tiny RAG. Ships a script that lists the concepts a snippet uses in learning order and traces execution step by step like Python Tutor.
python-comprehension-refactorer
Refactor Python loops into list, dict and set comprehensions and generator expressions, and unroll comprehensions that are too clever: filtering, mapping, nested-loop flattening, conditional expressions, walrus in comprehensions, dict inversion, grouping with defaultdict/Counter, lazy generators for sum/any/all/max, streaming large files, itertools (chain, islice, groupby, batched, pairwise, accumulate, product), scoping rules, side-effect anti-patterns and performance (when map/filter or numpy/pandas vectorization win). AI-engineering focus: building prompt lists, batching texts for embeddings, filtering retrieved chunks by score, id->embedding dicts, flattening tool-call results, lazy dataset streaming and token counting.
python-dataclass-converter
Convert boilerplate Python classes, dicts, tuples and JSON payloads into dataclasses, or into pydantic v2 BaseModel, TypedDict, NamedTuple, attrs or msgspec when that fits better. Covers every @dataclass option (frozen, slots, kw_only, order, unsafe_hash, match_args, weakref_slot), field() (default_factory, init=False, repr, compare, hash, metadata), __post_init__, InitVar, KW_ONLY, inheritance and default ordering, replace(), asdict deep-copy pitfalls, hashing/equality, serialization and validation. AI focus: LLM request/response and chat Message types, RAG chunks with embeddings (numpy eq gotcha), eval records, configs, typed OpenAI/Anthropic JSON.
python-exception-handling-designer
Design and review Python exception handling: custom exception hierarchies with retryable flags, what to catch where, EAFP vs LBYL, chaining with raise ... from, else/finally, logger.exception, add_note, ExceptionGroup and except* with asyncio.TaskGroup, warnings, asserts vs validation, FastAPI error translation, retries with backoff and jitter, timeouts, cancellation, pytest.raises, plus LLM-app failures: 429 rate limits with retry-after, 5xx overloaded, context-length exceeded, content filters, broken streams, malformed JSON repair loops, tool-call errors fed back to the model, partial embedding batch failures, CUDA OOM batch halving, vector DB outages, circuit breakers and model fallbacks.
python-idiomatic-code-reviewer
Review Python code for non-idiomatic, unpythonic or risky patterns and rewrite it into clean modern Python: PEP 8 / PEP 20, EAFP vs LBYL, iteration idioms (enumerate, zip, comprehensions, any/all), unpacking, f-strings, pathlib, truthiness, `is None`, mutable defaults, walrus, match statements, collections / itertools / functools, logging instead of print, with-blocks and resource handling. Includes AI-engineering checks: hardcoded API keys, time.sleep or requests inside async def, missing HTTP timeouts, unbounded retry loops, model clients built at import time, prompt string concatenation, sequential awaits, eval() on LLM output. Ships a 46-rule AST reviewer with severities and suggested rewrites.
python-type-hints-annotator
Add accurate, modern type hints to Python and fix mypy / pyright (pyrefly, ty) errors: builtin generics, X | None, TypeAlias and the 3.12 type statement, TypeVar and PEP 695 generics, Protocol, Callable / ParamSpec / Concatenate, TypedDict (Required, NotRequired, ReadOnly), Literal, Final, ClassVar, overload, Annotated, TypeGuard / TypeIs, Self, Never, TypeVarTuple, async types, Iterable vs Sequence vs Mapping, TYPE_CHECKING, stubs, py.typed, gradual strictness. AI-engineering focus: typed LLM message dicts and tool schemas, pydantic vs TypedDict, numpy NDArray, torch / jaxtyping shapes, streaming generators, async clients, callbacks, generic VectorStore protocols, typed config. Ships an AST audit: coverage gate, legacy-typing rewrites, inferred types.