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_size → window_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
ParallelResultconstructor takesstatus: RunStatus, replacing the two bools — the contradictorytimed_out=True, aborted=Truestate becomes unrepresentable rather than rejected.result.ok=status is COMPLETED and not failures. The headline fix: a truncated run can never reportok.COMPLETEDmeans the source was exhausted and every admitted item resolved — items may still have failed; that is exactly theCOMPLETED-but-not-okstate, and it is meaningful..timed_out/.abortedstay as derived properties (back-compat reads keep working);.statusis the source of truth..completeproperty =status is COMPLETED(the reviewer's "source exhausted" signal, independent of item failures).__repr__shows status whenever notCOMPLETED-and-ok.
3. Result invariant hardening¶
ItemResult: an explicitly-passederrormust be anExceptioninstance, an explicitly-passedvaluemust not be passed alongside it — kills theerror=None"success" hole.raise_on_failure(): attach provenance viaexc.add_note(f"item {idx}")(PEP 678). Notes don't change exception types, soexcept* ConnectionError:matching is untouched. When a stablecheckpoint_keywas 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()-pathpickle.loadsso a corrupted row raisesCheckpointErrorwith the delete-to-start-fresh instruction — matchingput()and the schema checks. - Permissions: create new checkpoint files
0o600(pre-create withos.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 verifypy.typedships (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
Makefiletwine 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¶
- Regression tests first — pin today's footguns as failing-is-correct
tests before touching code:
.okon timeout,ItemResult(error=None), NaN rate, corrupted checkpoint row, decoratorNoneoverride. Each test names the production bug it prevents. RunStatus/.okredesign + result hardening (items 2, 3).window_sizerename (item 1) — mechanical, lands after the semantic work so review diffs stay separable.- Typing prototype gate → decorator typing + sentinel (items 4, 5).
- Policy validation + checkpoint hardening (items 6, 7).
- CI/release + docs/metadata (items 8, 9).
- 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
Limitersurface 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 (infersIterable[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_ParallelDecoratorcarrying 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()staysIterable[tuple[Any, ...]]— binding a ParamSpec to a tuple type (*Tsinside a ParamSpec list) is invalid in both checkers. - Bound methods can never bind item types —
selfmakes the function non-unary before__get__splits it off;_BoundParallel[R]keepsRtyped with loose items. - Default-arg functions (
def f(x, y=2)) match the unary overload (T= first param) —.map()gains typing;.starmap()remains loose, sof.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/misleadingExceptionGroup— pinned, including theok_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_EXCLreports them as EEXIST, the suppress treated that as "existing file", SQLite followed the link. Fixed:lstatonFileExistsError, non-regular files fail closed withCheckpointError; two symlink tests added. - P1 (code review): async
concurrency: int | Nonebecame a typed lie under presence-sentinel — explicitNonecrashed the engine. Fixed:intin all three async TypedDicts, clear runtimeValueError, typing + runtime tests. - P1 (code review): the plan's own
timeout/task_timeoutnumeric commitment was unmet —timeout=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 theAbortedbranch 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 radius —rl = 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 toAnyin mypy. Not fixable at the type level (bidirectional inference vs. the overload set); pinned as documented limitations intyping_assertions.pywith a pyright suppression, README claim narrowed to "named functions".functools.partial: pyright binds correctly, mypy collapses toAny— 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_failuredocstring. - 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.stopunreachable-assert, rename completeness, decorator factory parity vs 0.7, docs examples under new.oksemantics, checkpoint fresh-file detection with the pre-created empty file, CI shell correctness. Design review:.okredesign holds against the common idiom; keyword-only/*args/callable-class typing all resolve correctly.