Bobobk

Practical Python, Linux and automation guides

Python Parallel Batches: Bound Pending Tasks and Report Every File Failure

2026-10-23 Technology
Python Parallel Batches: Bound Pending Tasks and Report Every File Failure
On this page

Submitting one Future per file can create a large backlog when a batch contains thousands of files, even with only four worker threads. Another common failure is to submit work without retrieving results, leaving file errors out of the final report.

This beginner-to-intermediate tutorial builds a read-only UTF-8 text line counter using the Python standard library. It reads a path list incrementally, bounds retained tasks, records each file’s success or failure, and gates the next automation step with an exit status.

1. Method: bound both threads and retained tasks

The existing process pools, thread pools, and coroutines introduction explains concurrency models. Here we connect a thread pool to a runnable batch entry point with these rules:

  1. --workers defaults to 4 and accepts 1–16, limiting concurrently running worker threads.
  2. pending holds at most 2 × workers Futures, including queued, running, and completed results awaiting collection. At the limit, collect results before reading more paths.
  3. Emit one JSON record per completed file, including missing files and invalid UTF-8. Any file failure makes the final exit status 1.

The official ThreadPoolExecutor documentation defines the worker limit. wait(..., FIRST_COMPLETED) waits until at least one task finishes. Our code implements the separate submission bound.

Threads demonstrate independent file reads here; faster execution is not guaranteed. Disk throughput, file sizes, decoding, and counting costs affect results. Start with 2–4 threads and measure your workload.

2. Prepare the environment and input

Use an existing Python 3.9 or newer interpreter; no third-party packages are needed. Examples were verified on Linux with Python 3.11.4. Commands work in bash or zsh.

  1. Create a fresh demo directory and keep subsequent commands in the same terminal and directory. Choose a new name when repeating the experiment:
python3 --version
mkdir parallel-batch-demo
cd parallel-batch-demo
printf 'alpha\nbeta\n' > a.txt
printf 'gamma' > 'b space.txt'
printf 'a.txt\nb space.txt\n' > good.txt
  1. Save the next section as count_files.py, or download the complete script.
  2. good.txt is a BOM-free UTF-8 manifest with one path per line. Relative paths use the manifest’s directory as their base; absolute paths also work. Empty lines are skipped and spaces in filenames are preserved. This format cannot represent filenames containing newlines, and repeated paths are processed repeatedly.

Use this example only with completed ordinary local text files that remain unchanged during reads. Keep FIFOs, device files, and remote mount paths that may block indefinitely out of the manifest.

3. Complete implementation

"""Count UTF-8 text lines with bounded parallel submissions. Python 3.9+."""
import argparse
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
import json
from pathlib import Path
import sys


def worker_count(value):
    number = int(value)
    if not 1 <= number <= 16:
        raise argparse.ArgumentTypeError("workers must be 1..16")
    return number


def count_lines(path):
    with path.open(encoding="utf-8", errors="strict") as source:
        return sum(1 for _ in source)


def collect(pending):
    done, _ = wait(pending, return_when=FIRST_COMPLETED)
    failed = 0
    for future in done:
        path = pending.pop(future)
        record = {"type": "file", "path": str(path)}
        try:
            record.update(ok=True, lines=future.result())
        except (OSError, UnicodeError, ValueError) as exc:
            failed += 1
            record.update(ok=False, error=f"{type(exc).__name__}: {exc}")
        print(json.dumps(record), flush=True)
    return len(done), failed


def run(manifest, workers):
    manifest = manifest.absolute()
    pending = {}
    processed = failed = peak_pending = 0
    with manifest.open(encoding="utf-8", errors="strict") as source:
        with ThreadPoolExecutor(max_workers=workers) as pool:
            for line in source:
                name = line.rstrip("\r\n")
                if not name:
                    continue
                path = manifest.parent / name
                pending[pool.submit(count_lines, path)] = path
                peak_pending = max(peak_pending, len(pending))
                if len(pending) >= 2 * workers:
                    count, errors = collect(pending)
                    processed += count
                    failed += errors
            while pending:
                count, errors = collect(pending)
                processed += count
                failed += errors
    print(json.dumps({
        "type": "summary", "processed": processed, "failed": failed,
        "workers": workers, "peak_pending": peak_pending,
    }), flush=True)
    return 1 if failed else 0


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("manifest", type=Path, help="UTF-8 path list, one per line")
    parser.add_argument("--workers", type=worker_count, default=4)
    args = parser.parse_args()
    try:
        return run(args.manifest, args.workers)
    except (OSError, UnicodeError, ValueError) as exc:
        print(f"FATAL: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())

count_lines() reads files without modifying them. Empty files have zero lines; a final segment without a newline still counts as a line. Text reading handles LF, CRLF, and CR. These are text lines, not CSV records: CSV fields can contain newlines, as explained in splitting CSV by records.

The main thread writes reports; workers only return line counts. Future.result() re-raises worker exceptions, so the collection step catches expected file and encoding errors. Unexpected programming errors still terminate the program with an exception instead of fabricating a successful summary.

Reports follow collection order; tasks collected together have no fixed order. Match records by path, not by their position against the manifest. A summary appears only after all work has been collected. If the manifest itself cannot be opened or decoded, stderr receives FATAL and the exit status is 1. stdout may contain partial file records without a complete summary.

4. Run and verify healthy input

  1. View help and process both files. Redirection creates or truncates the report, so choose a fresh report path:
python3 count_files.py --help
python3 count_files.py good.txt --workers 2 > good-report.jsonl
cat good-report.jsonl

The two type: file records have lines values of 2 and 1. The final record is:

{"type": "summary", "processed": 2, "failed": 0, "workers": 2, "peak_pending": 2}
  1. Verify by path rather than output order:
python3 - <<'PY_CHECK'
import json
from pathlib import Path
rows = [json.loads(line) for line in
        Path("good-report.jsonl").read_text(encoding="utf-8").splitlines()]
files = rows[:-1]
assert {Path(row["path"]).name: row["lines"] for row in files} == {
    "a.txt": 2, "b space.txt": 1,
}
assert all(row["ok"] for row in files)
assert rows[-1] == {
    "type": "summary", "processed": 2, "failed": 0,
    "workers": 2, "peak_pending": 2,
}
print("PASS: both files counted; pending bound respected")
PY_CHECK

peak_pending measures the peak number of retained Futures, not the number of threads actually running simultaneously. Even a quickly completed file remains in pending until collected.

5. Verify failures and exit status

  1. Create invalid UTF-8 and combine healthy, missing, and wrongly encoded files in one manifest:
python3 - <<'PY_DATA'
from pathlib import Path
Path("bad.bin").write_bytes(b"\xff\n")
Path("mixed.txt").write_text(
    "a.txt\nmissing.txt\nbad.bin\nb space.txt\n", encoding="utf-8"
)
PY_DATA
if python3 count_files.py mixed.txt --workers 2 > mixed-report.jsonl; then
  printf 'UNEXPECTED: all inputs accepted\n'
else
  batch_status=$?
  printf 'exit=%s\n' "$batch_status"
fi
python3 - <<'PY_CHECK'
import json
from pathlib import Path
rows = [json.loads(line) for line in
        Path("mixed-report.jsonl").read_text(encoding="utf-8").splitlines()]
assert rows[-1]["processed"] == 4
assert rows[-1]["failed"] == 2
assert rows[-1]["peak_pending"] <= 4
bad = {Path(row["path"]).name: row["error"]
       for row in rows[:-1] if not row["ok"]}
assert bad["missing.txt"].startswith("FileNotFoundError:")
assert bad["bad.bin"].startswith("UnicodeDecodeError:")
print("PASS: two failures recorded; healthy files still completed")
PY_CHECK

Expect exit=1 and PASS. All four entries receive reports, two fail, and healthy files still finish. Failure details appear in each JSONL error field, so empty stderr does not establish success.

  1. Gate the next task with the exit status, then check invalid arguments. Run the last two lines interactively; scripts using set -e should put expected failures inside an if condition:
if python3 count_files.py good.txt --workers 2 > next-report.jsonl; then
  python3 -c 'print("NEXT: every listed file passed")'
else
  printf 'STOP: inspect the report and stderr\n' >&2
fi
python3 count_files.py good.txt --workers 0
printf 'exit=%s\n' "$?"

The healthy manifest prints NEXT; replacing it with mixed.txt prints STOP. --workers 0 returns 2, the default error exit for argparse argument validation. An empty manifest returns 0 with processed: 0 and failed: 0; separately require a positive count if your task needs input.

6. Troubleshooting and limits

Symptom Concrete action
Memory grows with many files Keep incremental submission instead of building a list of every Future; an individual very long text line can still consume substantial memory.
FileNotFoundError Resolve relative paths from the manifest’s directory and check spelling and case; keep input files in place throughout the run.
UnicodeDecodeError Export UTF-8 from the source tool or explicitly change the input encoding in the code; avoid hiding damaged bytes with errors="ignore".
More threads make it slower Reduce --workers and compare elapsed time on identical input; thread count does not guarantee speed.
Waiting never ends This example has no task timeout or forced thread termination; identify blocking input and add the relevant operation’s own timeout mechanism.
Too many open files Reduce concurrency and follow file handle leak troubleshooting to check whether other code leaves files open.

A Future limit is not a process memory limit: individual manifest lines, text lines, and system buffers still consume memory. This script keeps collecting a batch after individual failures. It does not stop on the first file error, retry tasks, persist progress, or forcibly cancel threads.

Summary

Bound both running worker threads and retained Futures, and retrieve every result. Identify reports by path, aggregate failures, and return a reliable exit status before connecting the checker to another task. Test healthy and broken inputs together, then tune concurrency for your disk and workload.

Tags

1024 2fa ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena concurrency configparser configuration copy cprofile cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools difflib disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management filecmp filenames files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow ipaddress jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics multiprocessing mutation mysql nanobert nanobodies networking networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print probability prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures sorting spl sql sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml totp tracemalloc trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode upsert urllib usdt utf-8 uv venv vhhbert vite wallet web-tools webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 迅雷解析

友情链接