pygolem
Spawn tiny little servants, running things in parallel.
Before
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch_all(fn, items, workers=10, max_retries=3, rate_limit=5):
values = [None] * len(items)
errors = {}
remaining = list(enumerate(items))
timestamps = []
lock = threading.Lock()
def rate_limited(item):
while True:
with lock:
now = time.monotonic()
timestamps[:] = [t for t in timestamps if now - t < 1]
if len(timestamps) < rate_limit:
timestamps.append(now)
break
time.sleep(0.05)
return fn(item)
for attempt in range(max_retries + 1):
with ThreadPoolExecutor(max_workers=workers) as executor:
futures = {executor.submit(rate_limited, item): i for i, item in remaining}
remaining = []
for future in as_completed(futures):
index = futures[future]
try:
values[index] = future.result()
except Exception as e:
remaining.append((index, items[index]))
errors[index] = e
if not remaining or attempt == max_retries:
break
time.sleep(2 ** attempt)
return values, errors
results, errors = fetch_all(fetch_url, urls)
Roughly 40 lines, and this version doesn't even validate its own inputs, guard
against the bool-is-int trap, or cap thread count safely at scale.
After
from pygolem import threads
results = threads.map_parallel(fetch_url, urls, workers=10, retries=3, rate_limit=("token", 5))
results.values # successful outputs, in original input order
results.errors # {index: {"type": ..., "message": ...}} for items that failed permanently
Four lines. Same results. Ordered output, isolated per-item failures, retries with backoff, and a global rate limit.
What it is
pygolem wraps concurrent.futures.ThreadPoolExecutor with the things a
real parallel workload eventually needs:
- Ordered results:
.valuesalways matches your input order, even though threads finish in whatever order they finish in. - Per-item error isolation: one failing item never crashes the batch.
- Retries with backoff, plus
retry_ifto skip retrying failures you already know are permanent. - Global rate limiting: cap total throughput across the entire pool, not per-worker, with a choice of token bucket, leaky bucket, or sliding window.
Dependency-free, just the standard library.
Installation
pip install pygolem
Quick example
from pygolem import threads
import requests
def fetch(url):
return requests.get(url, timeout=5).status_code
urls = [
"https://example.com",
"https://httpbin.org/status/500", # will fail, then retry
"https://github.com",
]
result = threads.map_parallel(
fetch, urls,
workers=3,
retries=2,
backoff=("exponential", 0.5),
rate_limit=("token", 5),
)
print(result.values) # [200, 500, 200]
print(result.errors) # {1: {"type": "ConnectionError", "message": "..."}}
API
threads.map_parallel(fn, items, workers=None, retries=None, retry_if=None, backoff=("linear", 1), rate_limit=None)
| Parameter | Type | Default | Description |
|---|---|---|---|
fn |
callable | — | Function to call on each item. |
items |
list / tuple | — | Inputs to process. Must be non-empty. |
workers |
int | min(32, len(items)) |
Max concurrent threads. |
retries |
int | 0 |
Retry attempts per item after a failure. |
retry_if |
callable | None |
callable(exception) -> bool — return False to stop retrying that item. |
backoff |
(mode, delay) |
("linear", 1) |
mode is "linear" or "exponential". |
rate_limit |
(algorithm, value) |
None |
algorithm is "token", "leaky", or "sliding"; value is calls/sec across all workers. |
Returns a Data object: .values (results in input order, None for
permanent failures) and .errors ({index: {"type": str, "message": str}}).
Known limitations
- A stuck
fnhangs the whole call. There's no timeout yet, and Python can't force-kill a thread, so one item that never returns meansmap_parallelnever returns either. - Retries can repeat side effects. If
fndoes something real (like a request) and then fails afterward, retrying does that thing again. Useretry_ifto skip retrying failures you know already went through. - One slow retry delays everyone. The backoff wait happens between rounds, so already-successful items still sit and wait for it.
- More workers won't speed up CPU-heavy code. Threads share one GIL, extra workers mainly help waiting on network/disk, not crunching numbers.
Benchmarks
I/O-bound workload (time.sleep(0.01) per call), measured with
time.perf_counter(). Full breakdown in tests/02_speed.
| n | sequential | pygolem | speedup |
|---|---|---|---|
| 100 | 1.014s | 0.064s | 15.8x |
| 1000 | 10.260s | 0.525s | 19.5x |
Overhead over a raw ThreadPoolExecutor stays under 1% at scale. The
retry/error/ordering machinery is close to free.
The workers default is capped at 32 for a reason: an earlier version that
defaulted to workers=len(items) crashed at 100,000 items
(RuntimeError: can't start new thread). The capped default handles the
same load in 33s without crashing.
Tested
125 pytest tests, 0 failures — input validation, ordering, error isolation, retries, all three rate-limit algorithms, and a dedicated suite that proves out the known limitations. Full breakdown in tests/TEST_REPORTS.md.
pytest tests/ -v --tb=short
Roadmap
- Token-bucket and leaky-bucket rate limiting (alongside sliding-window)
- Conditional retries via
retry_if -
pygolem.aio:asyncio engine, with item-level backoff -
pygolem.processes:multiprocessing engine, with real cancellation of hung workers - Per-item timeout support
- Optional progress callback
Contributing
Contributions are welcome — see CONTRIBUTING.md for setup instructions, PR guidelines, and how to report bugs.
About the name
Inspired by Minecraft's copper golem. A small automaton that potters around doing small repetitive tasks on its own, tirelessly, until it needs a little maintenance. Not a bad description of a background worker.
License
Apache License 2.0. see LICENSE.
Metadata
Release files for pygolem 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pygolem-0.1.1.tar.gz | 22.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pygolem-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 35.8 kB
Release files / pygolem-0.1.1.tar.gz
| Download URL | pygolem-0.1.1.tar.gz |
|---|---|
| Size | 22.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4e907231659bbeedd960792f2db1e0c4ef5eea9ddaf85e4076b6005d25094073
|
|
BLAKE2b-256 checksum How to use checksums |
8a38002bd28889d4bb42fe272b2e76ed60836e9b83e71e435c9308b67f362c44
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 24, 2026.
Transparency logRelease files / pygolem-0.1.1-py3-none-any.whl
| Download URL | pygolem-0.1.1-py3-none-any.whl |
|---|---|
| Size | 12.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ef0ec35ccf0b10303381e85d8789c6e6b0c0fb404e0616c5e70f7506523ae27d
|
|
BLAKE2b-256 checksum How to use checksums |
f9bbe24a95da275a90cab84bbbc3c981e120cdae08ff7253a05b7dc46a379bdb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 24, 2026.
Transparency log