Skip to content

API consistency: no StopIteration from terminals; filter_map, unique, min_max and more - #23

Merged
virgesmith merged 4 commits into
mainfrom
feat/api-consistency
Sep 30, 2026
Merged

virgesmith merged 4 commits into
mainfrom
feat/api-consistency

Conversation

@virgesmith

@virgesmith virgesmith commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Why

A StopIteration escaping a map/filter callback silently ends the enclosing iteration instead of raising. Several terminal methods raised StopIteration for a missing item, so an innocent-looking chain could drop data without any error:

>>> Itr([[1, 2], [], [3]]).map(lambda g: Itr(g).reduce(max)).collect()
(2,)   # before: no error, group [3] silently lost

Breaking changes

  • position returns None when nothing matches, consistent with find (and Rust's Option).
  • Missing items raise the exception their builtin counterpart does:
    • nth past the end raises IndexError, as indexing a sequence does.
    • reduce on empty input raises TypeError, as functools.reduce does.
    • peek raises ValueError; it has no builtin counterpart, so this matches last, max and min.
  • next() deliberately still raises StopIteration, like the builtin. The README and skill now warn against calling it inside callbacks.
  • partition calls the predicate once per item (it was twice), buffering items for whichever side isn't being read. It is still lazy and still works on infinite input.

Additions (non-breaking)

  • New methods: filter_map, find_map, unique(key=None) (lazy and order-preserving), min_max(key=None), compress, take_last(n).
  • chain(*others) and interleave(*others) take any number of iterables.
  • zip(..., strict=), groupby(..., sort=False), optional sorted_by key, all()/any() default to truthiness, sum(start=)/prod(start=).
  • Itr.__repr__ shows the wrapped iterator without consuming it.
  • flat_map now uses chain.from_iterable.

Also

  • enumerate gains a doctest and a test. It had been covered only indirectly through the old position implementation, so changing position exposed the gap.
  • SKILL.md, README, relnotes, doc/apidoc.md (regenerated) and the examples notebook are updated. The notebook caught an out-of-range nth with suppress(StopIteration).
  • The examples notebook now demonstrates every public method (it covered 39 of 66). Five wrong statements are fixed, e.g. groupby was described as grouping consecutive runs, which is chunk_by, and all as never short-circuiting. All cells have been re-executed.
  • Relnotes list all of this under Unreleased, so the next release should be a minor bump.

Testing

ruff check, ruff format --check, ty check src and pytest all pass with 100% coverage (264 tests) locally on 3.14; pytest also passes on 3.12 and 3.13. There are new tests for each behaviour change, including a regression test for the silent truncation.

🤖 Generated with Claude Code

virgesmith and others added 4 commits September 30, 2026 08:22
…in_max and more

A StopIteration escaping a map/filter callback silently ends the enclosing
iteration, so e.g. Itr(groups).map(lambda g: Itr(g).reduce(max)) dropped
every group after the first empty one without error. Terminal methods now
report a missing item without StopIteration:

- position returns None when nothing matches (like find, and Rust's Option)
- peek, nth (out of range) and reduce (empty) raise ValueError, consistent
  with last/max/min; next() keeps StopIteration, as the builtin does

partition now calls the predicate once per item, buffering items for the
side not being read, instead of filtering a copy twice.

New: filter_map, find_map, unique, min_max, compress, take_last, and a repr.
Extended: chain/interleave take *others, zip(strict=), groupby(sort=False),
optional sorted_by key, all/any default to truthiness, sum/prod start.
flat_map uses chain.from_iterable.

enumerate gains a doctest and test: it was previously covered only via the
old position implementation. SKILL.md, README, relnotes, apidoc and the
examples notebook are updated to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow the builtins these methods stand in for, rather than using
ValueError across the board:

- nth past the end raises IndexError, as indexing a sequence does
  (a negative n remains a ValueError, as for itertools.islice)
- reduce on an empty iterator raises TypeError, as functools.reduce does

peek keeps ValueError: it has no builtin counterpart, and matches
last/max/min.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The notebook demonstrated 39 of the 66 public methods. Add short
examples for the other 27, under the existing section headings:
count/last/take_last, zip_longest, pairwise, chunk_by, filter_map,
compress, accumulate/scan, dedup/dedup_with_count/unique, sorted_by,
sum/prod/min/min_max, value_counts, find/position/find_map, eq,
is_sorted, map_while and peek/next_if. Also show the new chain and
interleave variadics and groupby(sort=False).

Fix statements that were wrong:
- groupby was described as grouping consecutive elements; it sorts
  first (that's chunk_by)
- interleave was said to stop at the shorter input (not since 0.3.0)
- all was said to always consume the entire iterator; it short-circuits
- an overrunning skip was said to raise StopIteration; it's lazy and
  yields nothing
- the nth(10) comment still assumed 1-based indexing

Includes a note that running the notebook needs ipykernel. All cells
re-executed, so outputs are current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@virgesmith
virgesmith merged commit 8c00850 into main Sep 30, 2026
9 checks passed
@virgesmith
virgesmith deleted the feat/api-consistency branch September 30, 2026 12:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant