Releases: OutSquareCapital/pyochain
Release 0.9.3
Release 0.9.3
This release fix wheels builds on windows with python >=3.14
Release 0.9.2
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_elsebetween{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/28Release 0.9.1
🚀 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
Iterobjects fell back to slow__next__()calls instead of delegating to the underlying iterator. Up to 10x faster. - Rust Migration: Moved
try_findimplementation 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
🚀 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
OptionandResultmethod calls. - API cleanup — Removed
Iter.join_withmethod (usecytoolz.itertoolz.joindirectly if needed)
🚨 API Changes
- Removed
Iter.join_with()— This method was a thin wrapper aroundcytoolz.itertoolz.join. Users can callIter.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/6Affected: 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 Pythonstrto RustStringconversion. - 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
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
🚀 pyochain 0.8.0 — Release Notes
Date: 2026-01-13 📅
🆕 Highlights
- Major Rust Migration —
OptionandResulttypes 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 likeIter.map(Option)orOption == x, and no regression on basic operations likeis_some()orunwrap() - 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__()andErr.__new__()now infer the "opposite" type parameter asAnyfor better ergonomics.
E.gOk(5)is now inferred asResult[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
.pyistubs 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/17Questions? Report issues on GitHub Issues
Release 0.7.0
🚀 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(),Setoperations,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 predicateextract_if(predicate)— Extract matching elements and remove themdrain(start, end)— Remove and return a range of elementstruncate(length)— ShortenVecto specified lengthextend_move(other)— ExtendVecby moving elements from another Vecconcat(other)— Concatenate an existingVecwith anotherVec|listin a new one.
Iter: New Operations
map_windows_star(length, func)— Sliding windows with argument unpackingscan(initial, func)— Stateful accumulation returningIter[U]
Dict: Views for Keys, Values, Items
keys()→PyoKeysView[K](replaceskeys_iter())values()→PyoValuesView[V](replacesvalues_iter())items()→PyoItemsView[K, V](replacesiter())
Set: New Operations
r_union(),r_intersection(),r_difference(),r_symmetric_difference()— Reversed set operationsis_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 onlyOption[T]withOption[T]__eq__(other)— Native equality operator supporting comparison with raw values and Python None
PyoCollection: Convenience
is_empty()— Check if collection is empty (moved fromDict)
⚠️ 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: Iand implemented__iter__()PyoCollection[I: Collection[Any], T]— stored_inner: Iand implemented__len__(),__contains__()
After:
PyoIterable[T]— fully abstract, requires__iter__()from subclassPyoCollection[T]— eager collections only, requires__iter__(),__len__(),__contains__()from subclassPyoIterator[T](NEW) — lazy iterators with comparison methods and aggregationsPyoSequence[T](NEW) — sequences with indexing and.get()methodPyoSet[T](NEW) — sets with union/intersection/difference operationsPyoMapping[T, V](NEW) — immutable mappings with keys/values/items viewsPyoMutableMapping[K, V](NEW) — mutable mappings with insert/remove operationsPyoMutableSequence[T](NEW) — mutable sequences with retain/truncate/extract_if/drain
What This Means:
- All comparisons moved to
PyoIterator(they acceptIterable[T]notSelf, and are fully lazy) PyoIterable.iter(self)now returnsIter(self)instead ofIter(self._inner)- All methods like
sum(),min(),max()now callselfdirectly instead ofself._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
_innerannotation 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._innerImpact:
The iteration API has been modified, and is now consistent with Dict.__iter__ which returns keys.
Before:
my_dict.iter() # Iterated over (key, value) pairsAfter:
>>> 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 = defaultAfter:
>>> 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
*othersvarargs,defaultparameter, 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 TrueMicro-Optimizations
- Reduced function call overhead in critical paths
- Better memory efficiency across collections
📚 Documentation
- 11 new trait reference pages —
PyoIterator,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
🚀 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 asSeq[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)
FalseThis 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())
NONEPerformance 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
🚀 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 refactor — Iter.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.Collectioninerhitance 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)
TrueIter.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()
NONEDict.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)
Truerepeat method — Both methods repeat Self as elements in a new Iter, but differ in internal implementation:
PyoCollection.repeat(n)returns anIter[Self]where each element is the entire (eager) collection repeatedntimes lazily. This was the previous behavior ofIter.repeat(), which collected eagerly the data original data in anhiddenway who might be confusing.Iter.repeat(n)returns anIter[Iter[T]]and is fully lazy (outer and inners Iter)
In other words:
- Iter.repeat return an
IteratorofIterator - PyoCollection.repeat an
IteratorofCollection
>>> 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
- ✅ Update
Iter.empty()→Iter.new()if you use it ⚠️ Updateis_sorted(key=func)→is_sorted_by(key=func)— Breaking change⚠️ UpdateIter.try_collect()for Option/Result types only — Breaking change, no moreU | Nonesupport- ✅ Update
Dict.contains_key()→Dict.contains()— Now unified across all collections - ✅ Consider using
Iter.from_ref()and.cloned()for better lazy copying - ✅ Leverage
PyoCollectiontrait if implementing custom collections
Questions? Report issues on our GitHub Issues page.
Release 0.6.4
🚀 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
frozendicttype from PEP 814 is introduced, it will might make sense to bring those methods back, since they internally always returned newdictinstances 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 behaviorEnhanced: 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.