A small execution layer over ThreadPoolExecutor: bounded memory, progress, retries, timeouts, cancellation.
Project description
tanda
A small execution layer over concurrent.futures.ThreadPoolExecutor for
running many I/O-bound jobs at once — with bounded memory, progress, retries,
timeouts, cancellation, and a Ctrl+C that actually works.
Tanda is Spanish for a batch, a round of work.
from tanda import Pool
with Pool() as pool:
results = pool.map(files, process)
Why
ThreadPoolExecutor gives you submit() and map(), and stops there.
Everything a real batch job needs on top — a progress line, retrying transient
failures, per-task timeouts, cancelling cleanly on Ctrl+C, not materializing a
10-million-item generator into a list of futures — gets reimplemented in every
script, slightly differently and slightly wrong each time.
tanda is not a prettier wrapper around submit(). It resolves the full
lifecycle of a concurrent batch, consistently:
submission → concurrency → progress → retries → timeout
→ cancellation → errors → shutdown
The basic case needs zero configuration. Everything else is opt-in:
from tanda import Pool, RetryPolicy
with Pool(workers=16) as pool:
results = pool.map(
files,
process,
progress=True,
retry=RetryPolicy(max_attempts=3, retry_on=(OSError,)),
task_timeout=30,
)
What you get
- Ordered results by default.
pool.map(["a", "b", "c"], fn)returns results in input order, regardless of completion order.imap()streams them in input order as they become ready;imap_unordered()yields in completion order when input order doesn't matter. - Bounded submission. Input iterables are consumed lazily; only a limited
window of tasks (roughly
workers × 4) is in flight at any time. A 100-million-item generator runs in constant memory instead of creating 100 millionFutureobjects up front. - Progress that counts items, not attempts. A task that fails twice and
succeeds on the third try advances progress by 1. Unsized iterables still
get a count, rate, and elapsed time — the iterable is never materialized
just to obtain a
len(). - Explicit retries.
RetryPolicy(max_attempts=3, retry_on=(OSError,))— attempt count instead of an ambiguousretries=, retry only for the exception types you list, with fixed or exponential backoff. - Honest timeouts.
task_timeoutmarks a task as timed out and applies your retry/error policy, but Python cannot kill a running thread — the underlying call may keep executing. tanda documents this instead of pretending otherwise (see When not to use tanda). - Cancellation and Ctrl+C. Pending tasks are cancelled, running tasks are
asked to stop, the executor shuts down, and
KeyboardInterruptis re-raised — no hung process, no wall of stack traces. - A shutdown you can bound. Leaving the
withblock cancels queued work and waits for what is still running.Pool(shutdown_timeout=30)caps that wait and raisesShutdownTimeoutnaming the stuck workers, instead of hanging on a task that never returns. - Errors that fail loudly. By default the first definitive failure raises
a
TaskErrorcarrying the item, its index, the underlying exception, the attempt count, and elapsed time.error_policy="collect"instead returns aBatchResultwith successes and failures separated — never a mixed list of results and exceptions.
When not to use tanda
- CPU-bound pure-Python work. Threads share the GIL; a thread pool will not speed up numeric loops, compression, or image transforms written in Python. Use processes — unless your library releases the GIL (NumPy, most I/O libraries).
- Hard timeouts. If a task must be forcibly killed at the deadline, threads are the wrong tool; use subprocesses.
- Task orchestration. Scheduling, DAGs, priorities, rate limiting, distributed execution, and persistent queues are explicit non-goals — that's Celery's job, not tanda's.
Installation
pip install tanda # no dependencies
pip install "tanda[tqdm]" # adds TqdmProgress
Python 3.10+. The package ships type information (py.typed), so your type
checker sees the annotated API without stubs.
Design
The design decisions — API surface, retry and timeout semantics, backpressure, the task state machine — are written down before the code:
- GUIDELINES.md — design principles and API decisions
- ARCHITECTURE.md — module layout, task state machine, threading model
- CONTRIBUTING.md — testing and benchmark requirements
- BENCHMARKS.md — measured overhead and the frozen budget
- examples/ — runnable scripts (progress bars, retries, callbacks)
Performance
On I/O-bound work — what tanda is for — the coordination overhead disappears
into the waiting: 1000 tasks of 5–20 ms land within 1% of raw
ThreadPoolExecutor.map. On 50,000 no-op tasks, where there is nothing to
wait for, tanda is 45% slower; that is the price of the lifecycle, and it is
published rather than hidden. Over a 1M-item input, submit-all peaks at
1.7 GB of futures against tanda's 0.1 MB.
Full numbers, methodology, and the frozen overhead budget: BENCHMARKS.md.
Status
The batch lifecycle described above is implemented and tested: bounded
submission, map(), imap(), and imap_unordered(), retries,
task_timeout and overall_timeout, cooperative cancellation, error
policies, progress reporting, and graceful shutdown. The suite is
adversarial rather than happy-path — 10k-item batches, the
timeout-versus-completion and error-versus-cancellation races, backpressure,
reentrancy — and runs on 3.10–3.13 across Linux and Windows. Measured
overhead and the frozen budget are in BENCHMARKS.md.
It is 0.x: the documented surface is what tanda commits to, and a change
to it comes with a minor bump and a CHANGELOG entry, not a
silent redefinition. A job-handle API and opt-in stats are next — see the
issues.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tanda-0.2.0.tar.gz.
File metadata
- Download URL: tanda-0.2.0.tar.gz
- Upload date:
- Size: 63.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dc5120b01ac27880bdf98efd6f9f7e74e87cbcf870a55cd67a541ca56cf843ec
|
|
| MD5 |
514bf12352ceaca74e67a2cf6c2da75e
|
|
| BLAKE2b-256 |
b2ad1053fcd472284415854f2f17bc8a5385f9e8d6b43710c50cbf4898482283
|
File details
Details for the file tanda-0.2.0-py3-none-any.whl.
File metadata
- Download URL: tanda-0.2.0-py3-none-any.whl
- Upload date:
- Size: 29.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e3736f89b31f3457f30ccdad1328333b635fd16aeec5cfd887ee9704c11111f7
|
|
| MD5 |
f293cc4ebbdab3d398770954a56bd118
|
|
| BLAKE2b-256 |
1c9a39779f566e7ec2bac05ea0f48e415ea06ae5f0fc910782fefa0dc5f495df
|