Skip to content

v0.8 — Honest Contract

One deliberate contract-breaking release. Everything conceptually wrong with the public API gets fixed here, together, before 1.0 freezes it — per DevX principle #7 (pre-v1 is the time) and the external 1.0-readiness review (2026-07). No new features; v0.9 carries those.

Theme: the vocabulary must not lie. A parameter named batch_size that is an admission window, a .ok that is True on a truncated run, an ItemResult that can be constructed in an invalid state, a "fully typed" claim the decorators don't meet — each is a small dishonesty that compounds into distrust. v0.8 removes all of them at once.

Ground truth (verified against 0.7.0 source)

# Finding Where Verified
1 batch_size is documented as "the admission window — not a chunk size"; the name says chunking core.py:438, devx-principles.md
2 .ok can be True on a timed-out unsized run; the code documents the footgun in __repr__ rather than fixing it result.py:122, result.py:217
3 ItemResult(0, error=None) passes the exactly-one check (has_error=True) and builds a "success" with value=None result.py:96-99
4 raise_on_failure() discards item indices — the ExceptionGroup has no provenance result.py:190
5 ParallelResult.__getitem__ returns Any result.py:205
6 RateLimit(float("nan")) accepted (nan <= 0 is False); inf accepted; Retry(backoff=-1) and non-finite max_delay accepted — no validation at all policies.py:39-47, policies.py:96-103
7 Decorator None means "inherit" — an explicit None cannot override a non-None decorator default (self-documented limitation) decorators.py:13-15, decorators.py:46
8 Decorated .map() accepts Iterable[Any]; .starmap() accepts tuple[Any, ...] — input types unchecked despite the "fully typed" README claim decorators.py:71, decorators.py:77
9 Checkpoint get() calls pickle.loads bare — corrupted rows leak arbitrary unpickling exceptions (put() wraps; the read path doesn't) checkpoint.py:274
10 Checkpoint files execute pickle on open; no user-facing trust warning anywhere in docs checkpoint.py, advanced-features.md
11 CI is Ubuntu-only despite documented fork/spawn platform differences; wheel/sdist never built or clean-installed; publishing is manual twine from a laptop ci.yml:10, Makefile:40
12 Roadmap says "Current (v0.5.0)"; package is 0.7.0 roadmap.md:23
13 Version string duplicated: pyproject.toml + pyarallel/__init__.py both

Work items

1. batch_sizewindow_size

Rename across all six public functions, the decorator TypedDicts, and every doc page. window_size over max_in_flight: in ordered streaming the bound is len(in_flight) + len(reorder_buffer) <= window — completed-but-unyielded items consume the window, so "in flight" would be its own small lie. The implementation and docs already say "admission window" everywhere.

Break, don't alias. Passing batch_size= raises TypeError: unexpected keyword argument naturally. Rationale (amended per design review — the original "install base is small" argument was weak both ways): pre-1.0, breaking clarity beats carrying a two-name surface, and an alias would keep the dishonest name alive in tutorials and completions for another cycle. CHANGELOG carries a prominent migration line.

2. RunStatus + corrected .ok

class RunStatus(enum.Enum):
    COMPLETED = "completed"    # source exhausted, every item resolved
    TIMED_OUT = "timed_out"
    ABORTED   = "aborted"      # max_errors tripped
    # CANCELLED reserved — cooperative stop lands in v0.9
  • ParallelResult constructor takes status: RunStatus, replacing the two bools — the contradictory timed_out=True, aborted=True state becomes unrepresentable rather than rejected.
  • result.ok = status is COMPLETED and not failures. The headline fix: a truncated run can never report ok.
  • COMPLETED means the source was exhausted and every admitted item resolved — items may still have failed; that is exactly the COMPLETED-but-not-ok state, and it is meaningful.
  • .timed_out / .aborted stay as derived properties (back-compat reads keep working); .status is the source of truth.
  • .complete property = status is COMPLETED (the reviewer's "source exhausted" signal, independent of item failures).
  • __repr__ shows status whenever not COMPLETED-and-ok.

3. Result invariant hardening

  • ItemResult: an explicitly-passed error must be an Exception instance, an explicitly-passed value must not be passed alongside it — kills the error=None "success" hole.
  • raise_on_failure(): attach provenance via exc.add_note(f"item {idx}") (PEP 678). Notes don't change exception types, so except* ConnectionError: matching is untouched. When a stable checkpoint_key was used, note the key instead of the index (design question — resolve in implementation; index is the floor).
  • __getitem__ overloads: int → R, slice → list[R].

4. Decorator typing — prototype gate first

May be the hardest item; gated by a throwaway prototype before any implementation:

@parallel()
def f(x: int) -> str: ...

f.map([1])          # must type-check
f.map(["wrong"])    # must be rejected by mypy --strict AND pyright
f.starmap([(1,)])   # must type-check
f.starmap([("w",)]) # ideally rejected (starmap may be unattainable)

The wrapper must become generic over the function's (single) parameter type as well as R — likely a _ParallelFunc[T, R] specialization selected by overloads on the decorator. If the type system cannot express it cleanly in both checkers, narrow the README/docs claim ("typed results and options; input item types are not checked") instead of shipping a runtime contraption. Negative assertions land in tests/typing_assertions.py either way.

5. Decorator override sentinel

Module-private _UNSET default for every decorator option; _merge_opts skips _UNSET instead of None. An explicit None then genuinely overrides a non-None decorator default (e.g. fetch.map(urls, retry=None) turns retry off). Removes the documented limitation in decorators.py:13-15.

6. Policy numeric validation

RateLimit: count finite and > 0; burst an int >= 1. Retry: backoff finite and >= 0; max_delay finite and >= 0; max_server_wait finite and >= 0 or None; attempts unchanged. math.isfinite throughout — NaN and inf both die in __post_init__ with the same actionable messages the existing checks use. Same pass covers timeout=/task_timeout= engine parameters if they share the hole.

7. Checkpoint trust, permissions, corruption

  • Docs: explicit warning in advanced-features.md and the API reference — a checkpoint file contains pickle; opening an untrusted one can execute arbitrary code. Treat checkpoint files like code, not like data.
  • Corruption: wrap the get()-path pickle.loads so a corrupted row raises CheckpointError with the delete-to-start-fresh instruction — matching put() and the schema checks.
  • Permissions: create new checkpoint files 0o600 (pre-create with os.open(..., O_CREAT | O_EXCL, 0o600) before SQLite touches the path; never chmod an existing file — the user may have intentional perms). Explicitly scoped small threat-model pass: symlink at the path, existing file, Windows (where mode bits are advisory — document, don't fight). This item is worthwhile, not trivial; timebox it and document what it does and does not defend against.

8. CI and release hardening

  • Wheel/sdist gate: build both, clean-install the wheel into a bare venv, import pyarallel, run a smoke test, and verify py.typed ships (mypy against the installed package on a tiny probe file).
  • macOS + Windows smoke lanes: one Python version each, test subset covering process executor (fork/spawn), checkpoint file handling, and the core engine. Ubuntu keeps the full matrix.
  • Trusted publishing: replace Makefile twine upload with a GitHub Actions release workflow using PyPI trusted publishing (OIDC) + artifact provenance. Manual laptop uploads end at 0.8.0.

9. Docs and metadata honesty

  • Roadmap rewritten around current reality (0.7 shipped; v0.8/0.9/1.0 as refined here).
  • Version single-sourced: [tool.hatch.version] path = "pyarallel/__init__.py", dynamic = ["version"] in [project].
  • README claims re-audited after the typing gate resolves ("fully typed" stays only if item 4 lands in full).

Sequencing

  1. Regression tests first — pin today's footguns as failing-is-correct tests before touching code: .ok on timeout, ItemResult(error=None), NaN rate, corrupted checkpoint row, decorator None override. Each test names the production bug it prevents.
  2. RunStatus / .ok redesign + result hardening (items 2, 3).
  3. window_size rename (item 1) — mechanical, lands after the semantic work so review diffs stay separable.
  4. Typing prototype gate → decorator typing + sentinel (items 4, 5).
  5. Policy validation + checkpoint hardening (items 6, 7).
  6. CI/release + docs/metadata (items 8, 9).
  7. Full four-review cadence (Codex review + adversarial, two independent agents) before merge, as in v0.5–v0.7.

Explicitly deferred to v0.9 (features, not contract)

Retry.for_http() helper; AsyncIterable sources; checkpoint_version= semantic token; cooperative stop + CANCELLED; minimal collected-result metadata (attempts/duration exposure — the streaming/collected asymmetry); reusable-HTTP-client example fixes; the deterministic local 429 → shared-pause → interrupt → resume demo.

Deferred to design-spike-only (pre-freeze check, no shipping)

  • Weighted/composite quotas — verify the Limiter surface doesn't preclude weighted acquisition later; 1.1 headline at most.
  • Streaming checkpoint — "cached computation" ≠ "sink committed"; at-least-once replay semantics must be crisp before any implementation.

Review amendments

A1 — Typing prototype verdict (gate resolved)

Two approaches prototyped against mypy --strict and pyright:

  • Approach A (self-type specialization)def map[T](self: _ParallelFunc[[T], R], ...). Pyright solves it fully; mypy cannot (infers Iterable[Never] on the positive case). Rejected.
  • Approach B (decorator overloads → unary subclass)@overload def parallel[T, R](fn: Callable[[T], R]) -> _UnaryParallelFunc[T, R], with a _ParallelDecorator carrying an overloaded __call__ so the factory spelling (@parallel(workers=4)) specializes too. Works in both checkers. Shipped.

Honest narrowings, verified inexpressible or structural:

  • Multi-arg .starmap() stays Iterable[tuple[Any, ...]] — binding a ParamSpec to a tuple type (*Ts inside a ParamSpec list) is invalid in both checkers.
  • Bound methods can never bind item typesself makes the function non-unary before __get__ splits it off; _BoundParallel[R] keeps R typed with loose items.
  • Default-arg functions (def f(x, y=2)) match the unary overload (T = first param) — .map() gains typing; .starmap() remains loose, so f.starmap([(1, 2)]) keeps working.
  • Negative assertions are enforced in CI by mypy's warn_unused_ignores; pyright honors ignore comments silently, so its agreement is prototype-verified. Adding a pyright CI step is part of work item 8.

A2 — Decorator default surface (scope finding)

Decorator defaults cover only workers/executor/rate_limit (async: concurrency/rate_limit); retry= etc. are per-call-only. Widening the default surface is deliberately NOT part of v0.8 — it is new API, not contract repair. v0.9 question.

A3 — Status-redesign regression pins (reviewer follow-up)

  • Repeated raise_on_failure() must not append duplicate PEP 678 index notes (mutating stored exceptions is observable) — pinned.
  • A truncated all-success run must raise a clear run-incomplete error from values(), iteration, AND indexing — never an empty/misleading ExceptionGroup — pinned, including the ok_values() escape hatch.

A4 — Checkpoint permissions approach (reviewer follow-up)

Create new checkpoint files securely at creation time (os.open(..., O_CREAT | O_EXCL | O_NOFOLLOW, 0o600)), not chmod-after-SQLite-open — the latter leaves a creation-time exposure window and a symlink concern. Never touch permissions of an existing file.

A5 — Four-review findings (Codex ×2 gpt-5.5, deep code review, adversarial design review)

All fixed on the branch unless noted:

  • P1 (code review + Codex adversarial): symlink defense was a no-op for dangling symlinks — O_EXCL reports them as EEXIST, the suppress treated that as "existing file", SQLite followed the link. Fixed: lstat on FileExistsError, non-regular files fail closed with CheckpointError; two symlink tests added.
  • P1 (code review): async concurrency: int | None became a typed lie under presence-sentinel — explicit None crashed the engine. Fixed: int in all three async TypedDicts, clear runtime ValueError, typing + runtime tests.
  • P1 (code review): the plan's own timeout/task_timeout numeric commitment was unmettimeout=float("nan") silently disabled the deadline while reporting COMPLETED. Fixed: _validate_timeout (finite, >= 0, or None) on both engines.
  • P2 (Codex + design review, converging): truncation now checked before per-item failures in values()/iteration/indexing — sized and unsized truncations previously raised different exception types for the same event, and the Aborted branch was dead code (aborted runs always carry failure markers). One ordering flip fixed both; the branch is now live and tested.
  • P0-doc (design review): the None-override blast radiusrl = maybe or None; f.map(urls, rate_limit=rl) silently disables rate limiting after upgrade. Dedicated CHANGELOG migration callout with the idiom spelled out. Runtime warnings considered and rejected (warning on documented legitimate use is noise).
  • P1 (design review): unannotated lambdas via parallel()(...) false-positive in pyright and collapse to Any in mypy. Not fixable at the type level (bidirectional inference vs. the overload set); pinned as documented limitations in typing_assertions.py with a pyright suppression, README claim narrowed to "named functions". functools.partial: pyright binds correctly, mypy collapses to Any — mypy-specific hole, documented same place.
  • P2 (code review): release workflow published without a test gate — a tag on a red commit would ship. Fixed: pytest step before build.
  • P2 (code review): cross-run note contamination — the same exception instance failing in two different runs accumulates an index note per run. Inherent to mutating shared exceptions; documented in the raise_on_failure docstring.
  • Release checklist (both Codex reviews): __version__ is the single version source now — bump to 0.8.0 immediately before tagging; the tag-guard blocks a mismatched tag.
  • Checked-and-clean (code review): RunStatus wiring at all construction sites, halt.stop unreachable-assert, rename completeness, decorator factory parity vs 0.7, docs examples under new .ok semantics, checkpoint fresh-file detection with the pre-created empty file, CI shell correctness. Design review: .ok redesign holds against the common idiom; keyword-only/*args/callable-class typing all resolve correctly.