Skip to content

Releases: OutSquareCapital/pyochain

Release 0.9.3

23 Apr 17:43

Choose a tag to compare

Release 0.9.3

This release fix wheels builds on windows with python >=3.14

Release 0.9.2

16 Jan 01:46

Choose a tag to compare

Release 0.9.2

🚀 pyochain 0.9.2 — Release Notes

Date: 2026-01-16 📅

🆕 Highlights

This release focuses on performance improvements through Rust migration and minor fixes to typing and keyword argument handling.:

  • Rust Migration: Moved 10 core iterator methods to Rust — Up to 3.09x faster for comparison operations, 2.39x faster for sorting checks
  • Type Safety Improvements: Enhanced Result.flatten() type inference for better IDE support and fewer false positives
  • Code Quality: Improved docstrings, internal tooling, and API consistency across Rust implementations
  • Kwargs bugfix: Fixed misalignment of keyword arguments in methods like map_or_else between {Ok, Some} and {Err, None} variants

🔥 Performance Improvements

Rust Migration: Iterator Comparison & Sorting Methods

Migrated 10 PyIterator methods from Python to Rust:

  • Lazy comparison operators: eq, ne, lt, gt, le, ge
  • Sorting checks: is_sorted, is_sorted_by
  • Functional operations: try_fold, try_reduce

This migration provides a median speedup of +105% across all methods, with marked improvements on comparisons and sorting operations.

Details

Performance results across 2,500 runs with 10 function calls each:

┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
┃ Category     ┃ Operation                        ┃ Runs ┃ New (μs, median) ┃ Old (μs, median) ┃ Speedup ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
│ eq_test      │ comp_eq                          │ 2500 │            349.6 │           1080.9 │   3.09x │
│ comparison   │ comp_lt                          │ 2500 │            368.5 │           1105.3 │    3.0x │
│ comparison   │ comp_ne                          │ 2500 │            366.1 │           1090.3 │   2.98x │
│ comparison   │ comp_gt                          │ 2500 │           368.35 │           1096.0 │   2.98x │
│ comparison   │ comp_ge                          │ 2500 │            368.3 │          1095.85 │   2.98x │
│ comparison   │ comp_le                          │ 2500 │           367.55 │           1082.5 │   2.95x │
│ is_sorted    │ is_sorted_asc_exit100            │ 2500 │            146.6 │            349.7 │   2.39x │
│ is_sorted    │ is_sorted_desc_exit100           │ 2500 │            151.7 │            358.5 │   2.36x │
│ is_sorted    │ is_sorted_asc_strict_exit100     │ 2500 │            150.0 │            350.2 │   2.33x │
│ is_sorted    │ is_sorted_desc_exit50            │ 2500 │             81.7 │            188.6 │   2.31x │
│ is_sorted    │ is_sorted_asc_exit50             │ 2500 │             78.4 │            180.2 │    2.3x │
│ is_sorted    │ is_sorted_asc_strict_exit50      │ 2500 │             79.7 │           182.25 │   2.29x │
│ is_sorted    │ is_sorted_desc_strict_exit100    │ 2500 │            150.9 │            342.9 │   2.27x │
│ is_sorted    │ is_sorted_desc_strict_exit50     │ 2500 │             82.3 │            181.5 │   2.21x │
│ is_sorted_by │ is_sorted_by_desc_exit50         │ 2500 │            376.7 │           711.45 │   1.89x │
│ is_sorted_by │ is_sorted_by_desc_exit100        │ 2500 │            744.6 │          1399.75 │   1.88x │
│ is_sorted_by │ is_sorted_by_asc_strict_exit50   │ 2500 │            379.6 │            712.6 │   1.88x │
│ is_sorted_by │ is_sorted_by_asc_strict_exit100  │ 2500 │            755.8 │           1402.0 │   1.85x │
│ is_sorted_by │ is_sorted_by_asc_exit100         │ 2500 │            759.9 │          1368.25 │    1.8x │
│ is_sorted_by │ is_sorted_by_asc_exit50          │ 2500 │            387.5 │            686.4 │   1.77x │
│ is_sorted_by │ is_sorted_by_desc_strict_exit50  │ 2500 │            386.0 │            678.1 │   1.76x │
│ is_sorted_by │ is_sorted_by_desc_strict_exit100 │ 2500 │            764.3 │           1341.2 │   1.75x │
│ try_reduce   │ try_reduce                       │ 2500 │            287.8 │            418.8 │   1.46x │
│ try_fold     │ try_fold                         │ 2500 │            293.0 │            418.7 │   1.43x │
│ try_fold     │ try_fold_conditional_logic       │ 2500 │            643.0 │            895.8 │   1.39x │
│ try_reduce   │ try_reduce_conditional_logic     │ 2500 │            650.0 │            888.6 │   1.37x │
│ try_fold     │ try_fold_string_accumulation     │ 2500 │            460.5 │            595.6 │   1.29x │
│ try_reduce   │ try_reduce_string_accumulation   │ 2500 │            452.8 │            585.0 │   1.29x │
└──────────────┴──────────────────────────────────┴──────┴──────────────────┴──────────────────┴─────────┘

Median speedup: 2.05x
New wins: 28/28

Release 0.9.1

15 Jan 02:15

Choose a tag to compare

🚀 pyochain 0.9.1 — Release Notes

Date: 2026-01-15 📅

🆕 Highlights

This release is a critical bugfix and performance improvement release:

  • CRITICAL FIX: Fixed a severe performance regression introduced in 0.7.0 where iterating over Iter objects fell back to slow __next__() calls instead of delegating to the underlying iterator. Up to 10x faster.
  • Rust Migration: Moved try_find implementation to Rust — Achieving +15-30% speedup across all use cases.
  • Code Quality: Various internal Rust code improvements for maintainability.
  • Documentation: Fixes on API reference generation. Website is now live again.

🐛 Critical Bug Fix

Iter Performance Regression

Issue: After migrating to abstract traits in 0.7.0, the Iter.__iter__() method was accidentally removed.
This caused Python to fall back to calling __next__() repeatedly when Iter objects were passed to functions expecting Iterator objects, resulting in dramatically slower performance.

Expected behavior: Iter wraps any iterable and converts it to an iterator by calling iter() on it, storing the result in self._inner. When Iter.__iter__() is called, it should directly return self._inner rather than self. This delegation is crucial because:

  • It bypasses Iter's Python-level __next__() implementation
  • The underlying iterator's native (often C-level) __next__() is called instead
  • This allows maximum efficiency regardless of the original iterable type (list, tuple, generator, etc.)

Fix: Restored Iter.__iter__() to properly delegate to self._inner, restoring original performance characteristics. Also explicitly calls __iter__() in PyoIterator provided methods as additional safeguard.

Impact: Any code passing Iter objects to functions that iterate over them (e.g., list(iter_obj), tuple(iter_obj), func(iter_obj) where func iterates) will see performance return to expected levels.


🚀 Performance Improvements

try_find Migration to Rust

Moved Iter.try_find() implementation from Python to Rust for better performance across all scenarios:

┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
┃ Category ┃ Operation                   ┃ Rust (s, median) ┃ Python (s, median) ┃ Speedup ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
│ try_find │ find_at_middle              │           0.0067 │             0.0087 │   1.30x │
│ try_find │ find_at_end                 │           0.0132 │             0.0163 │   1.24x │
│ try_find │ find_not_found              │           0.0148 │             0.0168 │   1.14x │
│ try_find │ find_error_late             │           0.0143 │             0.0171 │   1.20x │
│ try_find │ find_with_complex_predicate │           0.0020 │             0.0025 │   1.23x │
└──────────┴─────────────────────────────┴──────────────────┴────────────────────┴─────────┘

Median speedup: 1.23x
Rust wins: 5/5

This act as a proof-of-concept for further migrations of iteration algorithms which are not calling Python itertools, builtins functions, or cytoolz functions (as those are already very efficient).


Release 0.9.0

14 Jan 19:59

Choose a tag to compare

🚀 pyochain 0.9.0 — Release Notes

Date: 2026-01-14 📅


🆕 Highlights

This release mostly focuses on performance improvements:

  • Checkable & Pipeable Rust migration — Core traits now implemented in Rust with PyO3 for up to +25% Performance gains on methods who take Callable arguments, and no regression observed on simpler ones (e.g Checkable.then_some())
  • Fix on expect_* methods — Conversion fix resulting in significant speedups (+126%) on error handling methods (expect, expect_err).
  • Optimisations across the board for Option and Result — General optimizations on lifetime annotations in Rust resulting in a modest, but real, +1% performance gain across the board for Option and Result method calls.
  • API cleanup — Removed Iter.join_with method (use cytoolz.itertoolz.join directly if needed)

🚨 API Changes

  • Removed Iter.join_with() — This method was a thin wrapper around cytoolz.itertoolz.join. Users can call Iter.into(cytoolz.itertoolz.join()) directly to achieve the same functionality.

🚀 Performance Improvements Details

Checkable & Pipeable Migration

Rust handles function argument unpacking more efficiently than Python, with gains across all argument patterns:

  • No arguments/ Positional arguments: ~+25% faster
  • Keyword arguments: ~+10% faster
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
┃ Category   ┃ Operation    ┃ Rust (s, median) ┃ Python (s, median) ┃ Speedup ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
│ ok_or_else │ no_args      │           0.0004 │             0.0005 │   1.27x │
│ ok_or_else │ args         │           0.0005 │             0.0006 │   1.24x │
│ ok_or_else │ kwargs       │           0.0006 │             0.0007 │   1.13x │
│ then       │ no_args_then │           0.0004 │             0.0005 │   1.26x │
│ then       │ args_then    │           0.0005 │             0.0006 │   1.24x │
│ then       │ kwargs_then  │           0.0006 │             0.0007 │   1.11x │
└────────────┴──────────────┴──────────────────┴────────────────────┴─────────┘

Median speedup: 1.24x
Rust wins: 6/6

Affected: All objects in Pyochain benefit from this improvement, since they all inerhit from Checkable and/or Pipeable.

expect_* Methods Optimization

The optimization of PyO3 lifetime bounds for error messages resulted in significant speedups:

  • Before: Some parameters where annotated with String, resulting in implicit Python str to Rust String conversion.
  • Impact: This conversion was costly, and unecessary.
  • After: Direct &Bound<'_, PyStr> usage

Affected: {Option, Result}.expect(), Result.expect_err()


Questions? Report issues on GitHub Issues

Release 0.8.3

13 Jan 22:22

Choose a tag to compare

Release v0.8.3 - Rust-powered Option/Result with 3-10x speedups

This is a chore release for Pypi publish process.

Some issues with the new workflow had to be fixed, since part of the code now lives in Rust.

See https://github.com/OutSquareCapital/pyochain/releases/tag/0.8.0 for more details about the latest changes

Release 0.8.0

13 Jan 21:45

Choose a tag to compare

🚀 pyochain 0.8.0 — Release Notes

Date: 2026-01-13 📅


🆕 Highlights

  • Major Rust MigrationOption and Result types completely rewritten in Rust using PyO3
  • 3x-10x Performance Improvements — Up to 10x speedup on operations like {Option, Result}.transpose, 2-5x on core use-cases like Iter.map(Option) or Option == x, and no regression on basic operations like is_some() or unwrap()
  • Documentation Automation — New tooling to verify exports and generate reference docs, which in turn allowed to fix various issues in the documentation
  • Zero API Changes — Drop-in replacement with same public API, no breaking changes

✨ New Features & Changes

Rust-Backed Option & Result

Complete rewrite of Option[T] and Result[T, E] types in Rust:

  • Performance improvements — Significant speedups on a variety of operations (see details below)
  • Same public API — No breaking changes, drop-in replacement
  • First step towards more Rust implementations — Lays the groundwork for future Rust implementations of other parts of the library

Type System improvement

  • Ok.__new__() and Err.__new__() now infer the "opposite" type parameter as Any for better ergonomics.
    E.g Ok(5) is now inferred as Result[int, Any].
    Since they now live in stubs, this open the door for more "hacks" like this to deal with Python type system limitations.

Internal Improvements

  • Improved test coverage — Added comprehensive tests for Rust implementations
  • Stubs testing — Added a pytest plugin (from your dear maintainer) to ensure that docstrings in .pyi stubs are tested

📚 Documentation

  • Automatic export verification tool — New script who ensure that all documented classes/functions are exported, and automatically creates reference documentation from the codebase
  • Fixed missing exports — Several documented classes now properly exported in public API and documented
  • Improved docstrings — Added a few examples in docstrings during the migration

🚀 Performance Improvements Details

Basic operations like is_some(), unwrap(), xor() show minimal changes, as they were already just attribute access or boolean checks in Python and can't really be optimized further.

However, operations involving:

  • Higher-order functions (map, and_then, or_else)
  • Complex transformations (flatten, transpose, unzip)
  • Iterator integration (filter_map, chained operations)

Show significant speedups thanks to Rust's optimized execution and reduced Python call overhead.

Note that "complex transformations" involve in fact a few boolean checks and methods calls at most, so they were already quite fast in pure Python (as you can see below)
But, this observation open the door for a lot more optimization and Rust ports in the future.

The benchmark code can be seen in the commit 48770a7828e866be45fa7675d474c4b55704455d

Full Benchmark Results

┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
┃ Category           ┃ Operation                     ┃ Rust (s, median) ┃ Python (s, median) ┃ Speedup ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
│ Instantiation      │ Some(value)                   │           0.0001 │             0.0002 │   2.27x │
│ Instantiation      │ Dispatch to Some              │           0.0001 │             0.0004 │   3.28x │
│ Instantiation      │ Dispatch to None              │           0.0001 │             0.0001 │   1.53x │
│ Equality Checks    │ __eq__                        │           0.0001 │             0.0002 │   3.35x │
│ Equality Checks    │ eq_method                     │           0.0001 │             0.0001 │   1.74x │
│ Map with Closures  │ map (identity)                │           0.0002 │             0.0004 │   2.44x │
│ Map with Closures  │ map simple add                │           0.0002 │             0.0004 │   2.39x │
│ Chained Operations │ map -> filter -> map          │           0.0001 │             0.0002 │   1.72x │
│ Iter with Options  │ Iter.map(Option)              │           0.0005 │             0.0024 │   4.41x │
│ Iter with Options  │ Iter.filter_map (simple)      │           0.0044 │             0.0093 │   2.11x │
│ Iter with Options  │ Iter.map -> filter_map -> map │           0.0057 │             0.0183 │   3.22x │
│ Complex Methods    │ flatten                       │           0.0001 │             0.0003 │   4.93x │
│ Complex Methods    │ unzip                         │           0.0001 │             0.0005 │   4.32x │
│ Complex Methods    │ zip                           │           0.0001 │             0.0003 │   3.25x │
│ Complex Methods    │ zip_with                      │           0.0001 │             0.0004 │   2.49x │
│ Complex Methods    │ transpose (Option->Result)    │           0.0001 │             0.0010 │  10.21x │
│ Complex Methods    │ transpose (Result->Option)    │           0.0001 │             0.0009 │   8.98x │
└────────────────────┴───────────────────────────────┴──────────────────┴────────────────────┴─────────┘

Median speedup: 3.22x
Rust wins: 17/17

Questions? Report issues on GitHub Issues

Release 0.7.0

12 Jan 16:08

Choose a tag to compare

🚀 pyochain 0.7.0 — Release Notes

Date: 2026-01-12 📅

🆕 Highlights

  • Major architectural refactoring — Centralized common logic into trait hierarchy for better maintainability
  • 4 Breaking Changes — traits hierarchy and abstraction, Dict.iter(), Set operations, Iter.nth(), Iter.diff_at()
  • Numerous new features — New methods across Vec, Iter, Dict, Set, Option, and more
  • Performance improvements — Lazy comparisons and micro-optimizations

✨ New Features

Vec: In-Place Mutation Methods

All methods that modify the Vec in place are as memory-efficient as possible.
Compared to list methods like x.extend(y) followed by y.clear(), which copies the data from y to x before clearing y, these methods move the data directly without intermediate copies.

  • retain(predicate) — Keep only elements matching predicate
  • extract_if(predicate) — Extract matching elements and remove them
  • drain(start, end) — Remove and return a range of elements
  • truncate(length) — Shorten Vec to specified length
  • extend_move(other) — Extend Vec by moving elements from another Vec
  • concat(other) — Concatenate an existing Vec with another Vec|list in a new one.

Iter: New Operations

  • map_windows_star(length, func) — Sliding windows with argument unpacking
  • scan(initial, func) — Stateful accumulation returning Iter[U]

Dict: Views for Keys, Values, Items

  • keys()PyoKeysView[K] (replaces keys_iter())
  • values()PyoValuesView[V] (replaces values_iter())
  • items()PyoItemsView[K, V] (replaces iter())

Set: New Operations

  • r_union(), r_intersection(), r_difference(), r_symmetric_difference() — Reversed set operations
  • is_subset_strict(other) — Check strict subset (not equal)

PyoSequence: Safe Access

  • get(index)Option[T] — Safe element/slice access

Option: Enhanced Equality

  • eq(other) — Type-safe equality comparing only Option[T] with Option[T]
  • __eq__(other) — Native equality operator supporting comparison with raw values and Python None

PyoCollection: Convenience

  • is_empty() — Check if collection is empty (moved from Dict)

⚠️ Breaking Changes

1. Major Architectural Refactoring: Trait Hierarchy

Impact: — All subclasses of PyoIterable/PyoCollection must be updated

The trait hierarchy has been completely refactored to be fully abstract with specialized subtraits:

Before:

  • PyoIterable[I: Iterable[Any], T] — stored _inner: I and implemented __iter__()
  • PyoCollection[I: Collection[Any], T] — stored _inner: I and implemented __len__(), __contains__()

After:

  • PyoIterable[T] — fully abstract, requires __iter__() from subclass
  • PyoCollection[T] — eager collections only, requires __iter__(), __len__(), __contains__() from subclass
  • PyoIterator[T] (NEW) — lazy iterators with comparison methods and aggregations
  • PyoSequence[T] (NEW) — sequences with indexing and .get() method
  • PyoSet[T] (NEW) — sets with union/intersection/difference operations
  • PyoMapping[T, V] (NEW) — immutable mappings with keys/values/items views
  • PyoMutableMapping[K, V] (NEW) — mutable mappings with insert/remove operations
  • PyoMutableSequence[T] (NEW) — mutable sequences with retain/truncate/extract_if/drain

What This Means:

  • All comparisons moved to PyoIterator (they accept Iterable[T] not Self, and are fully lazy)
  • PyoIterable.iter(self) now returns Iter(self) instead of Iter(self._inner)
  • All methods like sum(), min(), max() now call self directly instead of self._inner
  • This allow to move a lot more methods in the base traits, providing more functionnalities for free to all subclasses
  • Custom subclasses must now implement dunder methods directly (no more _inner annotation magic)

Migration Example:

# Before
class MyList[T](traits.PyoIterable[list[T], T]):
    _inner: list[T]  # Auto-generated __init__

# After
class MyList[T](traits.PyoCollection[T]):
    _inner: list[T]
    def __init__(self, data: Iterable[T]) -> None:
        self._inner = list(data)
    def __iter__(self) -> Iterator[T]:
        return iter(self._inner)
    def __len__(self) -> int:
        return len(self._inner)
    def __contains__(self, item: object) -> bool:
        return item in self._inner

Impact:

The iteration API has been modified, and is now consistent with Dict.__iter__ which returns keys.

Before:

my_dict.iter()  # Iterated over (key, value) pairs

After:

>>> import pyochain as pc
>>> d = pc.Dict({"a": 1, "b": 2})
>>> d.keys().iter().collect()
Seq('a', 'b')
>>> d.values().iter().collect()
Seq(1, 2)
>>> # If you need old behavior:
>>> d.items().iter().collect() 
Seq(('a', 1), ('b', 2))

Reason: View classes provide pyochain methods and allow Dict.iter to be consistent with Dict.__iter__ which returns keys, and allow to simplify the Dict by removing redundant methods.


2. Set Operations Type Restrictions

Impact:

Set operations now only accept other collections.abc.Set compliant object instances, no longer Iterable.

Changed:

  • Before: *others: Iterable[Any] (multiple iterables, *args)
  • After: other: collections.abc.Set[T] (single parameter)

Before:

my_set.union([1, 2, 3])
my_set.union([2, 3], [4])

After:

>>> import pyochain as pc
>>> my_set = pc.Set([1, 2, 3])
>>> my_set.union({1, 2, 3})
Set(1, 2, 3)
>>> my_set.union(pc.Set([2, 3])).union({4})
Set(1, 2, 3, 4)

Reason: Allow to move these methods at the abstract PyoSet level in the trait hierarchy.


3. Iter.nth() Return Type

Impact:

nth() now returns Option[T] instead of raising an exception.

Before:

try:
    value = iter.nth(5)
except IndexError:
    value = default

After:

>>> import pyochain as pc
>>> pc.Iter([1, 2, 3]).nth(5).unwrap_or("default")
'default'

Reason: More functional API consistent with Rust.


4. Iter.diff_at() Signature

Impact:

Different behavior.

Changed:

  • Before: Multiple iterables via *others varargs, default parameter, raw values, signature was incorrect
  • After: Single iterable, returns Option[T] for missing elements

Before:

data = pc.Seq([1, 2, 3])
data.iter().diff_at([1, 2, 10, 100], default=None).collect()
# → Seq((3, 10), (None, 100))

After:

>>> data = pc.Seq([1, 2, 3])
>>> data.iter().diff_at([1, 2, 10]).collect()
Seq((Some(3), Some(10)),)
>>> # To unwrap values:
>>> data.iter().diff_at([1, 2, 10]).map(
...     lambda pair: (
...         pair[0].unwrap_or(None),
...         pair[1].unwrap_or(None)
...     )
... ).collect()
Seq((3, 10),)

Reason: Type-safe API with explicit Option-wrapped values for missing elements.


🚀 Performance Improvements

Lazy Comparisons

Comparison methods (eq(), ne(), lt(), le(), gt(), ge()) now use itertools.zip_longest() with sentinel values for fully lazy evaluation.

Stops early when difference found instead of consuming all elements.

Before:

# Eager: converts entire collections to tuple
return tuple(self._inner) == tuple(other._inner)

After:

# Lazy: stops as soon as difference found
sentinel = object()
for a, b in itertools.zip_longest(self, other, fillvalue=sentinel):
    if a is sentinel or b is sentinel or a != b:
        return False
return True

Micro-Optimizations

  • Reduced function call overhead in critical paths
  • Better memory efficiency across collections


📚 Documentation

  • 11 new trait reference pagesPyoIterator, PyoSequence, PyoMapping, PyoMutableMapping, PyoMutableSequence, PyoSet, and view classes
  • Enhanced core documentation
  • Improved interoperability guide

🔧 Migration Guide

Update Dict iterations

# Before
my_dict.iter()

# After
my_dict.items().iter()

Update Set operations

# Before
my_set.union([1, 2, 3])
my_set.union([2, 3], [4])

# After
my_set.union({1, 2, 3})
my_set.union({2, 3}).union({4})

Handle nth() Option return

# Before - exception
value = data.nth(5)

# After - Option
value = data.nth(5).unwrap_or(default)

Update diff_at() usage

# Before - varargs and default
data.iter().diff_at([1, 2, 10], [5, 6, 7], default=None)

# After - single iterable, Option return
data.iter().diff_at([1, 2, 10]).map_star(
    lambda l, r: (l.unwrap_or(None), r.unwrap_or(None))
)

Questions? Report issues on GitHub Issues

Release 0.6.6

09 Jan 20:36

Choose a tag to compare

🚀 pyochain 0.6.6 — Release Notes

Date: 2026-01-09 📅

🆕 Highlights

  • Iter.__bool__() for lazy iteration checks — Check if an iterator has elements without consuming them
  • Peekable now implements Checkable — Peeked values now returned as Seq[T] with truthiness checking
  • Removed deprecated Iter.empty() — Use .new() instead
  • Improved documentation — Enhanced trait descriptions and interoperability examples

🔄 API Changes & New Features

New: Iter.__bool__() Method

Added a __bool__() method to Iter for efficient emptiness checking without consuming elements:

>>> import pyochain as pc
>>> it = pc.Iter([1, 2, 3])
>>> bool(it)  # Check if iterator has elements
True
>>> it.collect()  # All elements still available
Seq(1, 2, 3)
>>> empty_it = pc.Iter([])
>>> bool(empty_it)
False

This method uses itertools.islice() and itertools.chain() to peek without consuming.

Checkable Methods Now Work Correctly on Iter

With __bool__() implemented, Iter now properly supports Checkable methods like .then() and .ok_or():

>>> import pyochain as pc
>>> pc.Iter([1, 2, 3]).then(lambda x: x.map(lambda v: v * 2).collect())
Some(Seq(2, 4, 6))
>>> pc.Iter([]).then(lambda x: x.map(lambda v: v * 2).collect())
NONE

Performance Consideration: Collect First When Appropriate

While __bool__() enables proper Checkable behavior on iterators, prefer collecting to a concrete collection first if Checkable methods are applied within iteration:

>>> import pyochain as pc
>>>
>>> # ❌ Less efficient: __bool__() called on each iteration in the map
>>> # This mean reconstructing the Iterator chain repeatedly
>>> result = (
...     pc.Iter([1, 2, 3])
...     .filter_map(lambda x: pc.Iter(range(x)).then_some().map(lambda s: s.sum()))
...     .collect()
... )
>>> result
Seq(0, 1, 3)
>>> # ✅ More efficient: collect first
>>> result = (
...     pc.Iter([1, 2, 3])
...     .filter_map(lambda x: pc.Iter(range(x)).collect().then_some().map(lambda s: s.sum()))
...     .collect()
... )
>>> result
Seq(0, 1, 3)

Calling __bool__() on Iter reconstructs the iterator chain, and doing this repeatedly within .map() or similar iteration operations is costly.

By collecting to eager collections (Seq, Vec, Set, etc.) beforehand, subsequent Checkable checks use __len__() which is a direct operation.

Breaking: Peekable Refactored

Peekable now inherits from Checkable and returns peeked values as Seq[T] instead of Iter[T]:

>>> import pyochain as pc
>>> data = pc.Iter([1, 2, 3]).peekable(2)
>>> data.peek  # Now returns Seq directly
Seq(1, 2)
>>> data.values.collect()  # Iter still includes peeked elements
Seq(1, 2, 3)
>>> # Checkable truthiness based on peeked values
>>> data.then_some().map(lambda d: d.peek)
Some(Seq(1, 2))

Migration: Code using .peek.collect() should be updated to use .peek directly since it's already a Seq.

Removed: Iter.empty() Deprecated Method

The deprecated Iter.empty() method has been removed. Use .new() from the PyoIterable trait instead:

>>> import pyochain as pc
>>> pc.Iter.new().collect()  # Before: pc.Iter.empty()
Seq()
>>> pc.Seq.new()  # Also works on other collections
Seq()

Questions? Report issues on our GitHub Issues page.

Release 0.6.5

09 Jan 16:04

Choose a tag to compare

🚀 pyochain 0.6.5 — Release Notes

Date: 2026-01-09 📅

🆕 Highlights

Iter.unzip() & Iter.repeat() now fully lazy — Memory-efficient evaluation using itertools.tee()
PyoCollection mixin trait — New shared collection trait
Iter.from_ref() & .cloned() — New lazy copying methods for efficient Iter branching
Sorting refactorIter.is_sorted() split into Iter.is_sorted() and Iter.is_sorted_by(key=...)
Iter.try_collect() narrowed — Now focuses on Option/Result types only
3 Breaking Changes — See section below for migration guide

🔄 API Changes & New Features

New Trait

PyoCollection is a new mixin trait for all Collection types

Provides:

  • .length() and .contains() methods
  • Implementations for __len__ and __contains__ dunders methods
  • collections.abc.Collection inerhitance and valid implementation

Iter.is_sorted(key=...) — Removed. Must use Iter.is_sorted_by(key=...) for key-based sorting checks.

>>> import pyochain as pc
>>> pc.Iter([1, 2, 3, 4]).is_sorted()
True
>>> pc.Iter(["1", "2", "3"]).is_sorted_by(key=int)
True

Iter.try_collect() — Now only accepts Iter[Option[T]] or Iter[Result[T, E]]. Removed support for Iter[T | None].
❌ Before - now broken

import pyochain as pc
pc.Iter([1, None, 3]).try_collect()

✅ After - must use Option

>>> import pyochain as pc
>>> pc.Iter([pc.Some(1), pc.Some(2), pc.Some(3)]).try_collect()
Some(Vec(1, 2, 3))
>>> pc.Iter([pc.Ok(1), pc.Ok(2)]).try_collect()
Some(Vec(1, 2))
>>> pc.Iter([pc.Some(1), pc.NONE, pc.Some(3)]).try_collect()
NONE
>>> # If you need old behavior, map to Option first
>>> pc.Iter([1, None, 3]).map(pc.Option).try_collect()
NONE

Dict.contains_key() — Replaced. Moved to PyoCollection.contains() for shared usability across all collection types.

>>> import pyochain as pc
>>> d = pc.Dict({1: "a", 2: "b"})
>>> d.contains(1)
True
>>> pc.Seq([1, 2, 3]).contains(2)
True
>>> pc.Set({1, 2, 3}).contains(3)
True

repeat method — Both methods repeat Self as elements in a new Iter, but differ in internal implementation:

  • PyoCollection.repeat(n) returns an Iter[Self] where each element is the entire (eager) collection repeated n times lazily. This was the previous behavior of Iter.repeat(), which collected eagerly the data original data in an hidden way who might be confusing.
  • Iter.repeat(n) returns an Iter[Iter[T]] and is fully lazy (outer and inners Iter)

In other words:

  • Iter.repeat return an Iterator of Iterator
  • PyoCollection.repeat an Iterator of Collection
>>> import pyochain as pc
>>> pc.Iter([1, 2]).repeat(2).map(list).collect()
Seq([1, 2], [1, 2])
>>> pc.Seq([1, 2]).repeat(2).map(list).collect()
Seq([1, 2], [1, 2])

Iter.from_ref() / .cloned() — New lazy methods for efficient Iter copying using itertools.tee()

>>> import pyochain as pc
>>> iter1 = pc.Iter([1, 2, 3])
>>> iter2 = pc.Iter.from_ref(iter1)
>>> # Both can be consumed independently
>>> iter1.take(2).collect()
Seq(1, 2)
>>> iter2.collect()
Seq(1, 2, 3)

Iter.empty() → Iter.new() — Consolidated at PyoIterable level (deprecated)

>>> import pyochain as pc
>>> pc.Iter.new().collect()
Seq()
>>> pc.Seq.new()
Seq()

⚡ Performance & Memory Improvements

Iter.unzip() — Now fully lazy using itertools.tee() instead of eager collection
Iter.try_collect() — Narrowed to Option/Result types only, reduced code paths and internal list optimization
Iter.repeat() — Fixed to be truly lazy with proper generator chaining


📚 Documentation Updates

• Added PyoIterable and PyoCollection reference pages
• Improved Iter.product() examples
• Various docstring improvements and warning block formatting fixes

🎯 Upgrade Recommendations

  1. ✅ Update Iter.empty()Iter.new() if you use it
  2. ⚠️ Update is_sorted(key=func)is_sorted_by(key=func) — Breaking change
  3. ⚠️ Update Iter.try_collect() for Option/Result types only — Breaking change, no more U | None support
  4. ✅ Update Dict.contains_key()Dict.contains() — Now unified across all collections
  5. ✅ Consider using Iter.from_ref() and .cloned() for better lazy copying
  6. ✅ Leverage PyoCollection trait if implementing custom collections

Questions? Report issues on our GitHub Issues page.

Release 0.6.4

09 Jan 02:16

Choose a tag to compare

🚀 pyochain 0.6.4 — Release Notes

Date: 2026-01-09 📅

🆕 Highlights

  • Methods Reorganization: PyoIterable → Iter
  • Deprecated Methods Removal from Dict and Option
  • Option.if_true() API Improvement
  • Various documentation improvements

🔄 API Changes

Migrated: from PyoIterable to Iter

Several methods have been moved from the PyoIterable base trait to Iter only, as they don't make sense on unordered collections like Dict and Set:

  • all_equal()
  • all_unique()
  • argmax()
  • argmin()
  • is_sorted()

Why? Dict, Set, and SetMut are unordered/non-comparable collections. Having sorting/uniqueness checks on them is semantically incorrect.

Migration:

# Before (on Seq, Vec)
seq.is_sorted()
seq.all_unique()
seq.argmax()

# After
seq.iter().is_sorted()
seq.iter().all_unique()
seq.iter().argmax()

Removed: Deprecated Dict Methods

The following deprecated methods have been permanently removed from Dict:

  • map_items()
  • map_keys()
  • map_values()
  • filter_items()
  • filter_keys()
  • filter_values().

Migration: Use .iter().map_star(...).collect(pc.Dict) or .iter().filter_star(...).collect(pc.Dict) instead:

# Before
d.map_keys(str.lower)
d.filter_values(lambda v: v > 0)

# After
d.iter().map_star(lambda k, v: (str.lower(k), v)).collect(pc.Dict)
d.iter().filter_star(lambda k, v: v > 0).collect(pc.Dict)

Note:
You can still chain dictionary transformations with .into() calls wrapping cytoolz functions, e.g.:

import cytoolz as cz

import pyochain as pc

res = pc.Dict.from_kwargs(a=1, b=2).into(
    lambda d: pc.Dict(cz.dicttoolz.keyfilter(lambda k: k == "a", d))
)
# Dict('a': 1)

Why?

  • Encourages explicit chaining via Iter.
  • Discourage long chain of transformation on dictionaries, which is inefficient since each step create a new intermediate dict -> hashing, no-lazy evaluation, no unpacking for *_item() methods, etc...
  • When the new frozendict type from PEP 814 is introduced, it will might make sense to bring those methods back, since they internally always returned new dict instances under the hood.

Removed: Option.from_() Deprecated Method

The deprecated Option.from_() method has been removed. Use Option() constructor instead:

# Before
pc.Option.from_(value)

# After
pc.Option(value)  # Same behavior

Enhanced: Option.if_true() Now Applies Predicate to Value

The Option.if_true() method has been improved to apply the predicate directly to the provided value, making it more ergonomic:

Before:

# Predicate received NO arguments - had to capture external state
pc.Option.if_true(42, predicate=lambda: external_check)

After:

# Predicate now receives the value itself
pc.Option.if_true(42, predicate=lambda x: x == 42)
pc.Option.if_true(42, predicate=lambda x: x > 0)

# Can call methods directly on the value
from pathlib import Path
pc.Option.if_true(Path("file.txt"), predicate=Path.exists)
pc.Option.if_true(Path("file.txt"), predicate=lambda p: p.exists())

Questions? Report issues on our GitHub Issues page.