Skip to content
v0.1.1latest release34 skills2 categories4 sub-categories79 tested scripts157 reference docsContact us

python-threadpool-executor-builder

Build correct, production-grade thread-based concurrency for I/O-bound Python: ThreadPoolExecutor submit/map/as_completed/wait (FIRST_COMPLETED, FIRST_EXCEPTION), Future callbacks, shutdown(cancel_futures), default max_workers (min(32, cpu+4), process_cpu_count in 3.13), initializer, BrokenThreadPool, Little's-law sizing, bounded submission and sliding windows, map buffersize (3.14), token-bucket RPM limiting, retries with backoff and jitter, cooperative cancellation, Ctrl-C, progress, thread-safe clients (requests.Session per thread, httpx, boto3, sqlite3), locks, queues, contextvars propagation, asyncio.to_thread and run_coroutine_threadsafe, free-threading, and testing. Ships a 19-rule linter and recipes. Use when fanning out LLM, embedding, scraping, S3 or vector-store calls from sync code, building an eval harness, or on lost exceptions, 429s, memory blowups from submit loops, hangs at exit, Ctrl-C ignored, 'cannot schedule new futures after shutdown' or 'Connection pool is full'.

Programming / Pythonv1.0.0Updated OCT 21 20267 references1 script

Build thread pools for I/O-bound work that are correct before they are fast: pick threads only when they are the right tool, size the pool from latency and the provider's rate limit instead of guessing, bound what is queued, make every exception surface, stop cleanly on Ctrl-C, and keep shared clients and state thread-safe. scripts/executor_tool.py check finds the mechanical mistakes statically (19 rules); scripts/executor_tool.py scaffold prints complete, runnable recipes (bounded map, RPM limiter, retries, progress, per-thread sessions, graceful shutdown, context propagation, and an LLM eval harness that combines them).

Trigger Conditions

Activate this skill when the user:

  • Fans out blocking I/O from synchronous code: LLM or embedding API calls, HTTP scraping for a corpus, S3 uploads/downloads, vector-store upserts, database reads, tool calls in a sync agent, Streamlit/Gradio callbacks
  • Writes or reviews ThreadPoolExecutor, executor.submit/map, as_completed, wait, Future, threading.Thread, threading.local, Lock, queue.Queue, asyncio.to_thread or run_in_executor
  • Builds an eval harness or batch job over thousands of prompts that must respect RPM/TPM limits
  • Sees exceptions disappear, memory grow while "submitting", 429 storms, a job that ignores Ctrl-C or never exits, RuntimeError: cannot schedule new futures after shutdown / after interpreter shutdown, BrokenThreadPool, Connection pool is full, discarding connection, SQLite objects created in a thread can only be used in that same thread, request ids missing from worker logs
  • Asks how many threads to use, how to add timeouts/retries/rate limits, or whether a client is thread-safe

Workflow

text
1. CLASSIFY  -> Is the hot path waiting on I/O? (CPU-bound -> GIL skill / processes; async codebase -> asyncio)
2. MEASURE   -> p50/p95 latency per call, provider limits (RPM, TPM, concurrent), item count, payload size
3. SIZE      -> workers = target_rate x p95 latency (Little's law), capped by rate limit and connection pool
4. SHAPE     -> one executor per job/app (with-block or lifespan), bounded submission, per-thread clients
5. HARDEN    -> results AND errors collected, timeouts, retries for transient errors only, rate limiter,
               stop event + cancel_futures on Ctrl-C, contextvars copied, logs carry thread + request id
6. CHECK     -> python scripts/executor_tool.py check src/ --strict
7. VERIFY    -> deterministic tests (barriers, fake clocks), no leaked threads, measured throughput vs limit

Step 1: Are Threads the Right Tool?

SituationRight toolWhy
Sync code, tens to a few hundred concurrent blocking calls (requests, boto3, DB drivers, SDK clients)ThreadPoolExecutorThreads release the GIL while blocked in sockets; no rewrite needed
Already async (FastAPI async def, httpx.AsyncClient, thousands of connections)asyncio, see python-asyncio-pattern-advisorOne thread, cheap tasks, structured cancellation
One blocking call inside async codeawait asyncio.to_thread(fn, ...)Keeps the event loop free
Pure-Python CPU work (parsing, chunking, scoring)Processes, see python-multiprocessing-pool-designerThe GIL serializes bytecode; threads give no speedup
CPU work in numpy/torch/tokenizers (Rust)/hashlib/zlib that releases the GILThreads can scale; measure firstDiagnose with python-gil-bottleneck-analyzer
CPU-bound and you control the interpreter (3.13t/3.14t free-threaded build)ThreadsNo GIL; check that your C extensions support it
Thousands of concurrent connections, long-lived streamsasyncioThreads cost a stack each and context switches
Program hangs, freezes or never exitspython-async-deadlock-debuggerDump every thread before changing code

Step 2: The Executor API in One Screen

CallBehaviour that matters
ThreadPoolExecutor(max_workers=None, thread_name_prefix="", initializer=None, initargs=())Default workers min(32, os.cpu_count() + 4) (3.8-3.12), min(32, os.process_cpu_count() + 4) (3.13+). Threads start lazily, one per submit until the max; idle threads are reused (3.8+)
submit(fn, /, *args, **kwargs) -> FutureNever blocks: the work queue is unbounded
map(fn, *iterables, timeout=None, chunksize=1, buffersize=None)Submits everything before returning (3.14 buffersize= bounds it); yields in input order; the first exception stops iteration and cancels queued items; timeout is a deadline for the whole iteration from the map() call; chunksize is ignored by threads
as_completed(fs, timeout=None)Yields futures as they finish; TimeoutError if the whole set is not done in time
wait(fs, timeout=None, return_when=ALL_COMPLETED)Returns (done, not_done), never raises on timeout; FIRST_COMPLETED, FIRST_EXCEPTION (acts like ALL_COMPLETED if nothing raises)
fut.result(timeout=None) / fut.exception(timeout=None)Re-raises the task's exception / returns it; TimeoutError only stops waiting, the task keeps running
fut.cancel()Succeeds only while the task is still queued; a running task cannot be cancelled or killed
fut.add_done_callback(cb)Runs in the worker thread that finished it, or immediately in the caller if already done; exceptions in cb are only logged (exception calling callback for ...)
shutdown(wait=True, *, cancel_futures=False)cancel_futures=True (3.9+) drops queued work; wait=False returns early but queued work still runs
initializer failsPool becomes broken: pending futures and later submits raise BrokenThreadPool (3.7+)
Interpreter exitWorkers are non-daemon (3.9+) and joined by an exit hook after draining the queue: queued work delays exit

concurrent.futures.TimeoutError is the builtin TimeoutError since 3.11. Full signatures, semantics and version tags: executor-api-reference.md.

Step 3: Size the Pool from Latency and Limits

Little's law: in-flight requests = throughput x latency. To sustain 10 requests/s at a p95 of 2 s you need about 20 calls in flight, so ~20-25 workers. More threads than that do nothing but queue on the rate limiter.

CeilingTypical valueConsequence
Provider rate limit (RPM/TPM, concurrent requests)e.g. 500 RPM = 8.3/sThe real cap: workers ~= rpm / 60 x p95_s x 1.2
HTTP connection poolrequests HTTPAdapter(pool_maxsize=10), httpx max_connections=100More threads than connections: Connection pool is full, discarding connection, new TLS handshakes
Server capacity / your DB poolDB pool size, vector DB write QPSMatch workers to it or you only add contention
Memory per in-flight itemdocuments, images, responsesBound in-flight items, not just workers
Threads themselves~8 MB virtual stack each, scheduler cost> 64-128 threads rarely helps; consider asyncio
python
def pool_size(rpm: float, p95_latency_s: float, connection_pool: int = 64, headroom: float = 1.2) -> int:
    """Workers needed to saturate a rate limit, capped by the connection pool."""
    needed = rpm / 60.0 * p95_latency_s * headroom
    return max(1, min(connection_pool, round(needed)))


print(pool_size(rpm=500, p95_latency_s=3.0))     # 30
print(pool_size(rpm=10_000, p95_latency_s=1.5))  # 64 (capped: raise the pool size or use asyncio)

The rate limiter, not the worker count, enforces the limit: always add a token bucket when a provider limits you.

Step 4: Bound What You Submit

submit() never blocks and map() drains its input up front, so a loop over a 10-million-line JSONL file queues 10 million futures before the first finishes. Keep a sliding window of at most N futures:

python
import itertools
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait


def call_llm(prompt: str) -> int:
    return len(prompt)                                   # stand-in for a blocking API call


def prompts():                                           # lazy source, e.g. a JSONL reader
    for i in range(10_000):
        yield f"prompt {i}"


total, max_in_flight = 0, 64
with ThreadPoolExecutor(max_workers=16, thread_name_prefix="llm") as ex:
    source = iter(prompts())
    pending = {ex.submit(call_llm, p) for p in itertools.islice(source, max_in_flight)}
    while pending:
        done, pending = wait(pending, return_when=FIRST_COMPLETED)
        for fut in done:
            total += fut.result()                        # surfaces task exceptions
        for p in itertools.islice(source, len(done)):    # refill one slot per finished task
            pending.add(ex.submit(call_llm, p))
print(total)                                             # 108890

On 3.14+ ex.map(fn, items, buffersize=64) gives the same bound for ordered results. A threading.BoundedSemaphore acquired before submit and released in a done-callback also works when you cannot restructure the loop. Patterns and trade-offs (ordered vs unordered, fail-fast vs collect, semaphore vs window vs queue.Queue(maxsize)): bounding-backpressure-and-rate-limits.md.

Step 5: Never Lose an Exception

An exception raised in a task lives on its Future until someone calls result() or exception(). Every future must be read, and every error must be kept or re-raised:

python
from concurrent.futures import ThreadPoolExecutor, as_completed


def embed(doc: str) -> list:
    if not doc:
        raise ValueError("empty document")
    return [float(len(doc))]


docs = ["alpha", "", "gamma"]
vectors, errors = {}, {}
with ThreadPoolExecutor(max_workers=4, thread_name_prefix="embed") as ex:
    futures = {ex.submit(embed, d): i for i, d in enumerate(docs)}
    for fut in as_completed(futures):
        i = futures[fut]
        try:
            vectors[i] = fut.result()
        except Exception as exc:                         # keep it: count, log, report, maybe retry
            errors[i] = exc
print(sorted(vectors), {i: repr(e) for i, e in errors.items()})
# [0, 2] {1: "ValueError('empty document')"}
PatternExceptionsUse
list(ex.map(fn, items))First one raised, later results lostAll-or-nothing jobs, small inputs
as_completed + try/except per futureEvery one keptBatch jobs, eval harnesses, ingestion
wait(fs, return_when=FIRST_EXCEPTION) then cancel the restFirst one, fail fastDependent work where one failure invalidates the batch
add_done_callback(cb)Only if cb calls result()/exception(); errors inside cb are just loggedProgress and metrics, not error handling

Step 6: Timeouts, Retries, Rate Limits, Cancellation

  • Threads cannot be killed. fut.result(timeout=5) stops waiting; the call keeps running and holds a worker. Put the real timeout in the I/O call (requests.get(url, timeout=(3.05, 30)), SDK timeout=), and use a threading.Event that long tasks check between steps.
  • Retry only transient errors (429, 5xx, timeouts, connection resets) with exponential backoff and full jitter, honouring Retry-After; never retry a 400 or a context-length error. Sleep with stop.wait(delay), not time.sleep, so shutdown is immediate.
  • Rate limit with one shared token bucket (RPM and, for LLMs, TPM): every attempt, including retries, takes a token. On a 429 with Retry-After, pause the whole bucket, not just one thread.
  • Circuit breaker for a dependency that is down: after N consecutive failures fail fast for a cool-down.
  • Ctrl-C: wait with a timeout in the main thread, then stop.set() and ex.shutdown(wait=True, cancel_futures=True); report partial results; exit 130.
python
import threading
import time
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait


def upload(part: int, stop: threading.Event) -> int:
    for _ in range(3):                                   # long task: check the stop flag between steps
        if stop.is_set():
            raise InterruptedError(f"part {part} stopped")
        time.sleep(0.01)
    return part


stop = threading.Event()
done_parts = []
with ThreadPoolExecutor(max_workers=4) as ex:
    pending = {ex.submit(upload, p, stop) for p in range(100)}
    try:
        while pending:
            done, pending = wait(pending, timeout=0.25, return_when=FIRST_COMPLETED)   # Ctrl-C lands here
            done_parts += [f.result() for f in done]
            if len(done_parts) >= 8:
                raise KeyboardInterrupt                  # simulate Ctrl-C for this example
    except KeyboardInterrupt:
        stop.set()
        ex.shutdown(wait=True, cancel_futures=True)
        print(f"stopped: {len(done_parts) >= 8}, cancelled: {sum(f.cancelled() for f in pending) > 50}")
# stopped: True, cancelled: True

Retry, token bucket, circuit breaker, deadlines, partial results and Ctrl-C in full: reliability-timeouts-retries-cancellation.md. Exception hierarchy design for retryable errors: python-exception-handling-designer.

Step 7: Thread Safety of Clients and Shared State

ObjectShare across threads?Do this
requests.SessionNot guaranteed (no documented thread-safety; cookies/adapters mutate)One Session per thread: threading.local() filled by the executor initializer
httpx.ClientDesigned to be shared (its pool is lock-protected)One client per app; set limits=httpx.Limits(max_connections=...) >= workers
openai.OpenAI / anthropic.Anthropic (sync)Built on httpx.Client; create once and reuseDo not create a client per call; check your SDK version's docs; per-thread clients are the conservative choice
boto3.session.Session / resourcesNoSession per thread; never create clients concurrently from the default session
boto3 low-level client (session.client("s3"))Yes (documented)Create it in the main thread before starting workers, then share
sqlite3.ConnectionNo by default (check_same_thread=True raises ProgrammingError)Connection per thread, or one writer thread fed by a queue.Queue
DB-API drivers (psycopg, etc.)Connections usually not; pools yesUse the driver's thread-safe pool (psycopg_pool.ConnectionPool)
logging loggers and handlersYes (handlers lock internally)Add %(threadName)s; slow handlers behind QueueHandler
queue.Queue, threading.Event, LockYesThe building blocks for coordination
Module-level list/dict/counterSingle operations are atomic in CPython; compound updates (+=, check-then-set) raceReturn values from tasks; otherwise guard with a Lock
threading.local valuesPer thread, but persist across tasks on the same pool threadNever store per-task state there
ContextVar valuesNot propagated into pool tasksex.submit(contextvars.copy_context().run, fn, *args)
python
import contextvars
import logging
import threading
from concurrent.futures import ThreadPoolExecutor

request_id = contextvars.ContextVar("request_id", default="-")
_local = threading.local()


def session() -> dict:
    """Per-thread client (a dict stands in for requests.Session())."""
    if not hasattr(_local, "s"):
        _local.s = {"owner": threading.current_thread().name}
    return _local.s


def fetch(url: str) -> str:
    return f"{request_id.get()} {url} via {session()['owner'].split('_')[0]}"


request_id.set("req-7")
with ThreadPoolExecutor(max_workers=2, thread_name_prefix="fetch", initializer=session) as ex:
    plain = ex.submit(fetch, "a").result()                        # noqa: TPE019 (the bug, for contrast)
    ctx = ex.submit(contextvars.copy_context().run, fetch, "b").result()
print(plain)   # - a via fetch
print(ctx)     # req-7 b via fetch

Locks, RLock, Condition, queues, contextvars and logging in depth: thread-safety-and-shared-state.md.

Step 8: Mixing Threads and asyncio

NeedUseNotes
Blocking call from a coroutineawait asyncio.to_thread(fn, *args, **kwargs) (3.9+)Default executor; copies contextvars
Blocking call with your own pool/limitsawait loop.run_in_executor(pool, functools.partial(fn, **kw))No kwargs, no context copy (wrap with copy_context().run)
Coroutine from a worker threadasyncio.run_coroutine_threadsafe(coro, loop).result(timeout=...)Never call .result() on the loop's own thread: deadlock
Notify the loop from a threadloop.call_soon_threadsafe(fut.set_result, value)asyncio objects are not thread-safe
Sync app that needs an async libraryOne loop in a background thread + run_coroutine_threadsafeStart once, stop with loop.call_soon_threadsafe(loop.stop)

asyncio.run() shuts the default executor down on exit (3.9+), waiting for running to_thread calls. The loop-thread deadlocks are covered in python-async-deadlock-debugger.

Step 9: Free-Threaded Builds and InterpreterPoolExecutor

  • On free-threaded CPython (3.13t experimental, 3.14t officially supported but still optional) threads run Python bytecode in parallel, so a thread pool can speed up CPU-bound work. Data races that the GIL used to hide (lost += updates, check-then-set) become frequent: fix TPE006 findings first. Diagnosis, scaling tests and extension compatibility: python-gil-bottleneck-analyzer.
  • concurrent.futures.InterpreterPoolExecutor (3.14) runs each worker in its own sub-interpreter with its own GIL: CPU parallelism in one process, but functions and arguments must be shareable (pickled), and many C extensions do not support sub-interpreters yet. For I/O it offers nothing over threads.

Symptom -> Cause -> Fix

SymptomCauseFix
Job "succeeds" but rows are missingFutures never read (TPE001), map not iterated (TPE005), except Exception: pass (TPE015)Collect results and errors per future
Memory climbs while submitting, nothing finishes for minutesUnbounded submit loop or map over a generator (TPE003/TPE004)Sliding window / semaphore / buffersize=
Ctrl-C ignored, or the job keeps calling the API after Ctrl-CMain thread blocked in result() with no timeout; shutdown(wait=False) without cancel_futures (TPE012)Wait with timeouts, stop event, cancel_futures=True
Process will not exit after the work is doneNon-daemon thread never stopped (TPE017), executor never shut down (TPE002), queued work draining at exitJoin/stop threads, shut down executors, cancel queued work
Hang under load onlyA task waits on futures of its own executor (TPE009): pool exhaustionSeparate pools per level or flatten; see the deadlock skill
429 storm, then bansWorkers > rate limit, retries without jitterToken bucket shared by all threads, backoff with full jitter
Connection pool is full, discarding connectionMore threads than pool_maxsize on one Session/hostRaise pool_maxsize (HTTPAdapter) or limits (httpx) to >= workers
Wrong/duplicated counts+= or check-then-set on shared state (TPE006)Return values and aggregate, or use a Lock
ProgrammingError: SQLite objects created in a thread...Connection shared with workers (TPE008)Connection per thread or a writer thread
Logs from workers lack the request id / traceContextVar not propagated (TPE019)copy_context().run per task
RuntimeError: cannot schedule new futures after shutdownSubmitting after with exit or shutdown()Keep submits inside the block; app-wide executor in lifespan
RuntimeError: cannot schedule new futures after interpreter shutdownSubmitting from atexit, __del__ or a daemon thread during exitStop producers before exit; join threads in main()
BrokenThreadPoolThe initializer raisedFix/guard the initializer; log its traceback (logged as "Exception in initializer")
8 threads no faster than 1CPU-bound pure Python (TPE018), a global lock, a shared single connection, or the rate limiterProfile; see the GIL skill; check pool and limiter

Live debugging (thread dumps, py-spy, faulthandler, naming threads, metrics): debugging-thread-pools.md.

AI-Engineering Patterns

JobShape
Eval harness over 10k prompts with an RPM limitSliding window + token bucket + retries + results/errors per item: scaffold --recipe llm-eval-harness
Embedding a corpus via APIBatch texts (provider max batch / token limit), pool of rpm/60 x latency workers, TPM bucket on token counts, write vectors in input order
Downloading/scraping a corpusSession per thread, per-host concurrency cap, robots/backoff, stream to disk, bounded window over the URL source
Vector-store upsertsBatches of 100-1000, a few workers (the DB is the ceiling), idempotent ids so retries are safe
S3 uploads of chunksOne boto3 client shared (created first), or TransferConfig(max_concurrency=...) which already uses threads
Parallel tool calls in a sync agentOne executor per agent process, wait(fs, timeout=step_deadline), tool errors returned to the model as results
Streamlit / Gradio appOne cached executor (st.cache_resource), never a pool per rerun/click, no Streamlit calls from worker threads

Worked, runnable versions: ai-engineering-threadpools.md.

Version Notes

VersionChange
3.2concurrent.futures
3.5max_workers optional (default cpu_count() * 5); map(chunksize=) (ignored by threads)
3.6thread_name_prefix
3.7initializer/initargs, BrokenThreadPool; contextvars module
3.8Default max_workers = min(32, cpu_count() + 4); idle workers reused before new threads start
3.9shutdown(cancel_futures=True); workers no longer daemon threads (joined by an exit hook); asyncio.to_thread
3.11concurrent.futures.TimeoutError is the builtin TimeoutError
3.13Default max_workers uses os.process_cpu_count(); free-threaded build (experimental)
3.14Executor.map(buffersize=); InterpreterPoolExecutor; free-threaded build officially supported; threading.Thread(context=) and sys.flags.thread_inherit_context

Output Format

When building or reviewing thread-pool code, answer with:

  1. Verdict - threads vs asyncio vs processes for this workload, with the reason (I/O wait share, library).
  2. Sizing - workers, max in-flight, rate limit and connection pool settings, with the Little's-law arithmetic.
  3. Findings - numbered, with location, TPE rule id when the linter flagged it, the failure it causes (lost data, hang, memory blowup, 429s, race) and severity.
  4. Code - complete, runnable before/after: executor lifetime, bounded submission, result/error collection, timeouts, retries, rate limiter, Ctrl-C handling, per-thread clients, context propagation.
  5. Verification - the tests to add (deterministic, see testing-threaded-code.md), a thread-leak check, and the throughput to expect vs the provider limit.

Scripts

bash
# Lint (AST only; never imports or runs the analysed code)
python scripts/executor_tool.py check src/                            # files or directories
python scripts/executor_tool.py check worker.py --json                # machine-readable
cat job.py | python scripts/executor_tool.py check -                  # stdin
python scripts/executor_tool.py check src/ --strict                   # exit 1 on any finding (CI gate)
python scripts/executor_tool.py check src/ --select TPE001,TPE005 --ignore pool-size-smell
python scripts/executor_tool.py check src/ --min-severity medium
python scripts/executor_tool.py check --list-rules                    # why + fix snippet per rule (--json too)

Rules: TPE001 future-never-read, TPE002 executor-not-shutdown, TPE003 unbounded-submit, TPE004 map-over-generator, TPE005 map-results-unconsumed, TPE006 shared-read-modify-write, TPE007 shared-container-mutation (low), TPE008 shared-unsafe-client, TPE009 executor-self-wait, TPE010 pool-size-smell (low), TPE011 result-without-timeout (low), TPE012 shutdown-nowait-at-exit, TPE013 daemon-thread-side-effects, TPE014 polling-future-done, TPE015 swallowed-future-exception, TPE016 executor-per-call, TPE017 thread-not-joined, TPE018 cpu-bound-thread-pool (low), TPE019 contextvar-not-propagated. Silence a line with # noqa: TPE010. Exit codes: 0 clean (or not strict), 1 findings with --strict, 2 usage/read/syntax error (directory walks warn and skip unparsable files).

bash
# Recipes: complete, typed, documented modules that run offline with stub work (verified with ast.parse)
python scripts/executor_tool.py scaffold --list
python scripts/executor_tool.py scaffold --recipe bounded-map > bounded_map.py && python bounded_map.py
python scripts/executor_tool.py scaffold --recipe llm-eval-harness --json      # {recipe, summary, code}

Recipes: bounded-map, rate-limited, retrying, progress, thread-local-client, graceful-shutdown (simulates Ctrl-C, exits 130), context-propagation, llm-eval-harness. Flags: --recipe NAME, --list, --json. Exit 2 for an unknown recipe or generated code that fails ast.parse.

Additional Resources

FileContent
executor-api-reference.mdEvery executor/Future/wait/as_completed/map detail, defaults by version, callbacks, shutdown and exit behaviour, BrokenThreadPool, threading.Thread basics
bounding-backpressure-and-rate-limits.mdLittle's law sizing, sliding windows, semaphores, bounded queues, map buffersize, token buckets for RPM/TPM, per-host limits
reliability-timeouts-retries-cancellation.mdPer-task timeouts, cooperative cancellation, retries with jitter, circuit breaker, progress, partial results, Ctrl-C
thread-safety-and-shared-state.mdClient thread-safety table, Lock/RLock/Condition/Event/Barrier, queue.Queue pipelines, threading.local, contextvars, logging, asyncio bridges, free-threading
ai-engineering-threadpools.mdEval harness, embedding batching, corpus download, vector upserts, S3 chunks, parallel tool calls, Streamlit/Gradio
debugging-thread-pools.mdEvery TPE rule with before/after, error messages explained, thread dumps, hangs at exit, slow pools, observability
testing-threaded-code.mdInline executors, barriers and events for determinism, fake clocks, mocking clients, leak detection, stress tests
More in Python

python-async-deadlock-debugger

Diagnose and fix hangs, deadlocks, livelocks and starvation in asyncio, threaded and multiprocessing Python: classify CPU spin vs blocked IO vs deadlock vs lost wakeup vs event loop blocked by sync code, collect evidence with faulthandler, py-spy dump, gdb py-bt, asyncio.all_tasks, python -m asyncio ps/pstree, debug mode and sys._current_frames, then fix lock-order cycles, non-reentrant asyncio.Lock re-acquisition, run_coroutine_threadsafe().result() on the loop thread, Queue.join without task_done, Condition lost wakeups, semaphore leaks, futures nobody resolves, nested event loops, thread pool exhaustion, multiprocessing Queue/fork deadlocks and shutdown hangs. Ships a static hang scanner and a watchdog that dumps every thread and asyncio task.

python-asyncio-pattern-advisor

Recommend, implement and review Python asyncio patterns: async vs threads vs processes (IO vs CPU-bound), coroutines vs tasks vs futures, TaskGroup, gather and return_exceptions, wait and as_completed, Semaphore and Queue worker pools with backpressure, asyncio.timeout vs wait_for, cancellation, shield and cleanup, token-bucket RPM/TPM rate limits for LLM APIs, retries with backoff and jitter, async generators and SSE token streaming, to_thread/run_in_executor, background task references, eager tasks, asyncio.run/Runner, uvloop, Jupyter, FastAPI async vs def routes, httpx.AsyncClient lifespan, async DB/vector-store clients, debug mode and pytest-asyncio.

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-context-manager-writer

Write correct Python context managers and find resource-handling bugs — __enter__/__exit__ semantics and exception suppression, contextlib (contextmanager, asynccontextmanager, ExitStack, AsyncExitStack, closing, aclosing, suppress, redirect_stdout, nullcontext, chdir), async with, re-entrancy, leaked files/connections/clients/pools/locks, and AI-engineering recipes: torch.no_grad/inference_mode/autocast, eval()/train() switches, GPU cache cleanup, token and latency meters, tracing spans, temp env vars, rate-limit semaphores, httpx/aiohttp client lifecycle, FastAPI lifespan, DB/vector-store transactions, atomic checkpoint writes, seeds and agent sandboxes.

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.