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. - A handle on the batch.
pool.start(items, fn)returns aJobyou can iterate,cancel(), and read counters from while it runs — submitted, running, completed, failed, timed out, cancelled, retried, elapsed, rate. The counters exist only on this path, so the plainmap()/imap()overhead is untouched. - 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(), imap_unordered(), and start(), 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. What is planned next is tracked in the
issues.
License
Metadata
Release files for tanda 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tanda-0.3.0.tar.gz | 70.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tanda-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 104.2 kB
Release files / tanda-0.3.0.tar.gz
| Download URL | tanda-0.3.0.tar.gz |
|---|---|
| Size | 70.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9afa0f36f0057dd6258ad7e4e7738a6bbcfa7931fe281dd3eaedf9a35eb18397
|
|
BLAKE2b-256 checksum How to use checksums |
d7b361adb4a5d605cb7d262b5e9ba58bd31e469f778b18f839fd15ff4b69da66
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.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 Aug 10, 2026.
Transparency logRelease files / tanda-0.3.0-py3-none-any.whl
| Download URL | tanda-0.3.0-py3-none-any.whl |
|---|---|
| Size | 34.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
52c1b3f10f9a8c7773e8dc3375c6561db2c9484b33187b4dfe6f07a5e92f75b4
|
|
BLAKE2b-256 checksum How to use checksums |
e179485be93410c7c24d2282ce07f5e2edd573245c4dfb5b30bc6c9986515751
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.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 Aug 10, 2026.
Transparency log