SortedKeySetandSortedKeyDictdon't expose anymore theirkeyattribute, as this is an implementation detail.{SortedKeyList, SortedList}::{add, update}methods have been replaced byappendandextend, respectively. Unlike the originalsortedcontainerslibrary, who raise runtime errors if those two are called. Allow to be more compliant with theMutableSequenceprotocol.{SortedKeyList, SortedList}::insertdoes not raise runtime errors anymore, and simply forward toappend. Index is ignored. Allow to be more compliant with theMutableSequenceprotocol.
"keys" sorted containers (i.e SortedKeyList, SortedKeySet, etc...) now require an explicit key argument, instead of providing by default an identity function.
Moreover, key is now the first argument, followed by the iterable argument, and they are both positional-only.
This allows performance improvements, simplify internal implementations, but most importantly, aligns more with the API choice of pyochain, which, unlike the original sortedcontainers library, provides those "key containers" as explicit classes, instead of dynamically creating them depending on the signature.
Because, frankly, why would you create a SortedKeyList with an identity function as key, when you can just use a SortedList instead?
-
Massive global improvements to
SortedListandSortedKeyList, and by extension sorted sets/dicts/views who use them internally. Detailed benchmarks will be provided in the near future. -
pprintwas used internally in many__repr__implementations, which is no longer the case. The "raw" repr of inner objects (e.glist,set, etc...) is now used, which means we swap a Python call with a CPython one.
When you domy_iter.for_each_star(f, *args), each element T of my_iter need to be passed first to f as the first argument, followed by the unpacked args.
This entails reconstructing a new data structure to hold said arguments.
The related logic has been optimized and fully refactored, with significant performance improvments, especially when the number of args is small (1 to 8), which is the most common use case.
See below for the benchmark results, with the number of args in the left column, the number of items in the Iterator in the header row, and the relative performance improvement in the cells.
2.00x means that the new implementation is twice as fast as the previous one.
| - | 10 | 100 | 1_000 | 10_000 | 100_000 |
|---|---|---|---|---|---|
| 2 | 1.25x | 1.37x | 1.40x | 1.43x | 1.43x |
| 4 | 1.28x | 1.37x | 1.37x | 1.37x | 1.39x |
| 8 | 1.31x | 1.45x | 1.47x | 1.47x | 1.47x |
| 16 | 1.21x | 1.26x | 1.26x | 1.25x | 1.22x |
| 32 | 1.00x | 1.17x | 1.19x | 1.20x | 1.19x |
- ``PyoIterator::map_windows_star` is 1.05x to 1.26x faster across all sizes tested, (small and big iterators, small and big window sizes). A single regression has been noted at 100k items with a window size of 32, which is planned to be fixed in a future release.
- typing: Relaxed the collector input type of
PyoIterator::collect. The constraint on the return typeR: Collection[Any]was artificial, and was preventing to usecollecton functions or types who indeed consume theIterator, but weren't strictly speaking aCollection(e.g polars DataFrames). In python, theFromIteratorequivalent is simplyCallable[[Iterator[T]], Any], and this is now reflected in the typing ofcollect. - typing:
Option::{is_some, is_none, unwrap, unwrap_or, unwrap_or_none}now have overloads to statically return the narrowed type when the variant is already known. - typing:
Result::and_thenhandles a change of Error type in the closure, and returns the correctResulttype. i.eResult[T, E].and_then(f: Callable[[T], Result[U, F]]) -> Result[U, E | F].
- typing: Ensured that all sorted containers classes are marked with
typing::{type_check_only, final}when appropriate.
- typing:
PyoIterator::{map_windows, map_windows_star}now correctly handle functions with varargs. - typing:
PyoIterator::for_each_starnow correctly handle functions with varargs and ParamSpec. - typing:
Iter::__new__missed overloads, which caused false positives with type checkers, for example, when providing a singleIterableargument. This is now fixed.
- Many changes to the internal rust structure to improve readability and reduce code-gen (and by extension, compile time).
- Added
panic="abort"flag on release builds to reduce binary size and improve performance. - Improved caching of CI workflows for performance.
-
Website: The return sections of the functions/methods docstrings is now correctly rendered thanks to @tecnolgd contribution in #95
-
Profiler scripts: Added a dedicated typer app to handle, analyze and plot benchmark results. Currently focused on
sortedcontainerscomparison, but can be extended to more in the future. -
Dev check scripts: Centralised the linting/formatting/etc... commands in a single entry point.
- Thanks to @tecnolgd contribution in #92,
Rangeis now aligned with its python counterpart constructor:Range(10)creates a range from 0 to 9, whileRange(1, 10)creates a range from 1 to 9. Thestepargument is optional and defaults to 1.
core::{Dict, Iter,Range, Vec, Seq, Set, SetMut}, and collections::{StableSet, Deque} now have flexible constructors: instantiate with no arguments, a variable number of positional elements, or a single iterable.
For example, Vec(1, 2, 3) or Vec([1, 2, 3]) will create the equivalent of [1, 2 ,3], while Vec() or Vec([]) will create an empty Vec.
Specialized constructors are available: from_iter for a single Iterable, of for a variadic number of args (or kwargs for Dict), or wrap, which replaces from_ref for concerned classes (e.g Vec.wrap(list())).
Use them if you want to keep performance identical to the previous version, or if you prefer explicitness.
Note that the overhead of the new constructors is very small, and the performance difference is negligible for most use cases. For example, the overhead of the attribute retrieval of Vec.from_iter is almost equal to the overhead of the Vec constructor itself. Only in situations like .iter().map(Seq.from_iter) can the overhead be noticeable, if you create a lot (+1k) of small (~10 elements) Seq instances. As always, benchmark your code before making any changes.
core::Peekablecan only be constructed fromPyoIterator::peekable.{SortedList, SortedKeyList}::loadis now fully private
PyoIterator::map_juxtis now 1.5× faster when provided with 1 function, 1.29× faster with 4–16 functions, and 1.13× faster with 64 functions. Results are consistent across iterables of sizes ranging from 10 to 10,000 items.Pyoiterator::zip_longestis now between 1.6x to 1.8x faster when provided with a single iterable, across tested sizes(10, 100, 1_000 and 10_000). Expect faster speed-ups with more arguments, as it's the overhead of creating theOptionvalues that has been greatly reduced.
Result::{__eq__, __ne__}are now implemented, allowing to compareResultinstances for equality.
Err::flattenwas actually not implemented. To prevent similar issues, the code for monads has started to be refactored to usepy_abcmacro.
- Added
Neverreturn types to a few methods ofPyoIteratorto clearly mark situations that will raise an error at runtime, and to help static type checkers to detect unreachable code.
- Relaxed the
Callablereturn type forOption::filtertoobject, since all python objects can be evaluated for truthiness. Option::__bool__returnNeverto comply with python typing standards regarding "always-failure" functions.OkandErrnow have their generics types toAnyas default. This allow to doOk[int, str]without seeing it inferred asResult[int, Any], it now becomesResult[int, str]as expected.- Various clean-ups and refactors in the Rust codebase, to improve readability and maintainability.
NOTE: the performance improvements and benchmarks are very likely understated, as they were done when the Rust migration was still partial.
- Full migration of the whole codebase to Rust
abc::PyoIterator::peekablehas been completely refactored and now aligns with Rust stdIterator, with the methodspeek,next_if,next_if_eq, andnext_if_map, all implemented in Rust.- documentation: All docstrings examples now use markdown code blocks instead of doctests, significantly improving readability. Many thanks to our contributor @tecnolgd for his help on this matter.
- typing: The variance of generics for all classes is now correctly handled and align with python stdlib. For example,
PyoMapping,SeqorSetare now covariant, whilstDictorVecstay invariant. collections::PyoCounter, pyochain version of python stdlibcollections::Counter. Herit fromPyoMutableMappinginstead ofdict, but behaves the same way.collections::{HeapMax, HeapMin}, pyochain reimplementation of python stdlibheapqmodule with an OOP, fluent API.- Full port of the
sortedcontainerslibrary to pyochain, rewritten with native rust data structures, and fully typed. Credits to Grant Jenks for the original implementation, as well as the maintainers of the corresponding stubs package.
PyoIterable is now only here for the iter method, and doesn't provide methods that implicitely convert the Iterable into a PyoIterator anymore. last and first, depending on the concrete subclass, were either redundant (first for PyoIterator who already has next) or not really useful (last for non-ordered Collections).
- API change:
PyoIterable::firsthas been deleted. This impacts all non-sequence types (PyoSequenceis unchanged). If you need the old behavior on anIteratoror aCollection, call.iter().next().unwrap()instead. - API change:
PyoIterable::lasthas been moved toPyoIteratorandPyoSequenceas separate methods without class inheritance. This impacts all customPyoIterablesubclasses who aren'tIterators norSequences. If you need the old behavior (e.g callinglaston aset-like collection), calliter().last()instead. - API change:
PyoIterator::batchhas been renamed toPyoIterator::batched. - Method removal:
PyoIterator::array_chunkshas been removed. UsePyoIterator::batchedinstead. - API change:
PyoIterator::map_withfunction argument now need to be the first argument, followed by the various iterables. It was previously a kword only argument at the end. Swap the order at call sites to migrate your code. - Removal:
PyoIterator::inserthas been removed. Replacey.insert(x)byIter.once(x).chain(y)to migrate your code. This makes thing clearer at both reading-order level, and semantics level, as this avoid treating anIteratoras "sort of" mutable collection. - Removal: Original
PyoIterator::repeathas been removed, andPyoIterator::from_repeatrenamed torepeat, i.efrom_repeatreplaces the oldrepeat. The original had complex semantics, niche use cases, without a real performance/memory benefit. To get the same behavior, use something likemy_iter.collect(Seq).pipe(lambda it: Iter.repeat(it, n)).map(lambda x: x.iter()). This also better align with rustIterator::repeatsemantics and naming. - Removal:
Iter::__bool__has been removed. UsePyoIterator::peekable::__bool__instead. This avoid implicitteeuse. - Removal:
Iter::{from_ref, cloned}have been removed. Usea, b = x.tee()instead (forcloned), ora, b = Iter(x).tee()(forfrom_ref), where a is the originalIterator, and b the cloned one. - Dunders methods (like
__add__,__mul__,__getitem__, etc...) now return new instances of the same class, instead of the underlying data structure when applicable, i.eVec + Vecreturns aVecinstead oflist. - imports: views-like classes are now back to being only imported from
abcmodule, to stay consistent with python stdlib. - Removal:
innerattribute is now a private implementation detail of pyochain, rather than something accessible from the public API. If you are confronted by code that refuses to handle, say, aMutableSequenceinstead of alist, 99% of the time, it's a good time to change it (or raise an issue to the author of the code). Python is meant to use duck typing, anddef foo(x: list):is, again, not a valid pattern, at runtime or for static typing, 99% of the time. In any case, you can simply dox.pipe(list)if needed, for example.
{Dict, SetMut, StableSet, Vec}::copyto create shallow copies of the instance, just likex.copy()for their respective underlying builtins.PyoSized,PyoReversible, andPyoContainernow inerhit fromCheckable, instead of relying on mixins heritance.get_itemis now aPyoMappingmethod instead of aPyoMutableMappingone.Dict,SetandSetMutare now fully compliant with their underlying data structure regarding the methods available.SetMut::difference_update.
- typing: added overloads to
PyoIterator::batchedto return precise tuple types for batch sizes up to 5 if strict isTrue. - typing: added overloads to
PyoIterator::productto return precise tuple types for up to 10 iterables. - API:
PyoIterator::accumulateis now aligned withitertools, with the possibility to not provide a function, in which case the default is to use addition. - API:
PyoIterator::productmissed it'srepeatargument. This is now fixed. - typing:
Iter,Option,Result,Peekable,Seq,Set, andabc::{PyoIterable, PyoIterator, PyoCollection, PyoSequence, PyoSet}types are now covariant in their generic type. - typing:
PyoSequence::getnow has correct generic types for slice overload. - typing: fixed overloads of
__get_item__forVec,SeqandRange. - typing: added overloads to
PyoIterator::filter_starto return precise tuple types for up to 3 elements when provided with a closure returning a TypeIs or TypeGuard specifying the narrowed tuple. - typing: relaxed closure return type for
PyoIterator::{filter, filter_star, filter_false}toobject, since all python objects can be evaluated for truthiness. - typing: The variance of generics for immutable classes (abstract and concrete) is now correctly handled and align with python stdlib. For example,
PyoMapping,SeqorSetare now covariant, whilstDictorVecstay invariant. - typing:
Dict::from_objectnow returnsDict[str, object]instead ofDict[str, Any]to avoid silencing errors with strict type checkers (e.g Pyrefly, Basedpyright) who forbid the use ofAnyin generic types. You can always usetyping::castif you need specific attribute access without runtime check AND no type checker errors. - typing:
ResultandOptionmethodtransposegive less false positives now. - typing: Thanks to covariance,
PyoIterator::flattenshould not give false positives anymore, since it seems there's no need anymore to manually maintain various overloads for nestedPyoIteratortypes. - typing:
PyoIterator::chainnow has overloads to return precise tuple types for up to 5 iterables, and handle covariance in all cases. - No-copy behavior for
Seqwhen created from an already existingSeq, matching Pythontuplebehavior. No more need to useSeq(seq.inner)to keep the same object,Seq(seq)is enough. Note that this doesn't impact performance much, since the underlyingtuple, where the data is stored, wasn't copied anyway, and theSeqwrapper is very lightweight in itself. This however help to makeSeqbehave more liketuple, and avoid confusion when usingisoperator on twoSeqinstances created from the same underlying data.
- Aligned
abc::PyoMappingViewheritance and generics logic with python builtins. PyoIterator::all_equalwithoutkeyargument didn't have a defaultNonevalue altough present in stubs, causing unexpectedTypeError.- Deleted
PyoIterator::_from_iterableclass constructor, which caused issues for subclassing. As of now, Each method return anIterupcasted toPyoIterator. Expect NO change in behavior. In a future release, each Iterator will have a dedicated struct (as long as it's faster than python builtins), hopefully allowing further performance improvements.
Instead of relying on automatic ABCs implementations, concrete classes now delegate to their underlying structure systematically.
abc::Reversible::revgot huge performance gains on concrete classes (Range,Vec, etc...), up to 28x faster, by implementing__reversed__dunder, which now return the C levelIteratordirectly, instead of relying on theabcauto implementation.Vec::__iter__has received the same type of optimization asrev. A quick benchmark with timeit show improvements between 25x to 45x faster on sizes ranging between 10k to 100k items.Dict::__contains__has received the same changes, but the performance isn't really changed, as the default ABC implementation is not that different from an optimized one (at least with adict).append,clearandextendreceived the same optimization onVecandDeque.
Many methods have been migrated to Rust.
See the table below for the performance improvements, with the number of items processed in each Iterator when testing.
Note that for unpack_into, even if it's worse on a relative basis on small Iterator's, the median absolute speed improvement on larger ones more than makes up for it, since on 10 items, it's 2.6 us (new) vs 2.42 us (old), while on 10k items, it's 229 us (new) vs 253 us (old).
What was observed is that the upper values outliers are much less frequent, especially on smaller sizes, where they have been reduced by a factor of 4.
The performance improvements on small Iterators are expected, but the slowdown (albeit minor) on large Iterators is very surprising.
There's less outliers, but their magnitude is worse, which can't be explained easily.
| Name | 10 items | 100 items | 1_000 items | 10_000 items | Note |
|---|---|---|---|---|---|
arg_max |
1.26x | 1.78x | 3.40x | 4.15x | Low items counts are likely higher after full Rust migration |
arg_min |
1.25x | 1.73x | 3.37x | 4.18x | Low items counts are likely higher after full Rust migration |
arg_max_by |
1.24x | 1.58x | 2.70x | 3.10x | Low items counts are likely higher after full Rust migration |
arg_min_by |
1.29x | 1.62x | 2.59x | 3.03x | Low items counts are likely higher after full Rust migration |
unpack_into |
0.93x | 0.99x | 1.09x | 1.11x | Low items counts are likely higher after full Rust migration |
zip_longest |
2.83x | 4.57x | 4.64x | 4.46x | Due to option creation in Rust |
unzip |
1.61 | 2.80x | 3.82x | 4.09x | Due to tuple access in Rust |
all_equal |
0.99x | 1.02x | 0.98x | 1.01x | Identical perf, Low items counts are likely higher after full Rust migration |
try_collect |
1.26x | 1.08x | 1.01x | 1.00x | Due to Vec creation in Rust, Low items counts are likely higher after full Rust migration |
partition |
1.24x | 1.05x | 1.02x | 1.02x | Due to Vec creation in Rust, Low items counts are likely higher after full Rust migration |
is_sorted |
1.12x | 1.07x | 1.01x | 1.00x | Due to default param now in Rust. |
group_by |
1.70x | 2.30x | 2.39x | 2.37x | - |
map |
1.08x | 1.03x | 1.00x | 1.04x | - |
accumulate |
1.11x | 1.05x | 1.04x | 1.00x | - |
reduce |
1.05x | 1.01x | 1.00x | 1.00x | - |
find_map |
1.16x | 1.02x | 0.98x | 0.99x | - |
take |
1.12x | 1.09x | 1.01x | 1.00x | - |
slice |
1.12x | 1.09x | 1.01x | 0.97x | - |
chain |
1.14x | 1.04x | 1.02x | 1.02x | The items here are the nb of Iterable arguments, not the total number of items in the base PyoIterator. |
product |
1.03x | 1.01x | 1.01x | 0.99x | The items here are the nb of Iterable arguments, not the total number of items in the base PyoIterator. |
next |
1.31x | 1.45x | 1.47x | 1.47x | The items here are the nb of calls to next in a loop. |
once_with |
1.75x | 1.83x | 1.84x | 1.84x | The items here are the nb of time we create a PyoIterator with once_with(), and then call next on it. |
map_with |
0.97x | 0.98x | 0.97x | 0.99x | Slight regressions -> we must reconstruct the tuple args for each call (func, self, others). |
tail |
1.20x | 1.24x | 1.23x | 1.19x | - |
Some::iter |
4.66x | 5.28x | 5.28x | 5.20x | Iter is directly created from new constructor, no getattr pattern. |
Null::iter |
5.87x | 7.13x | 7.07x | 6.96x | Iter is directly created from new constructor, no getattr pattern. |
Err::iter |
5.75x | 6.95x | 6.92x | 6.89x | Iter is directly created from new constructor, no getattr pattern. |
Ok::iter |
7.16x | 8.62x | 8.71x | 8.67x | It was calling self.ok() before internally. |
PyoIterable::iter |
1.16x | 1.19x | 1.18x | 1.17x | - |
Iter::__init__ |
1.82x | 1.93x | 1.93x | 1.92x | Also impact PyoMutableSequence::{extract_if, drain} and PyoReversible::rev |
Reversible::rev |
5.23x | 28.99x | 28.08x | 25.88x | For Range, Vec and Seq types. ABC's in itself is 2-3% faster on small iterators after Rust migration. |
Seq::__init__ |
1.29x | 1.14x | 1.01x | 1.01x | - |
Range::__init__ |
1.62x | 1.61x | 1.58x | 1.54x | - |
Seq::concat |
2.58x | 1.78x | 1.10x | 1.02x | - |
-
Rust migration and logic optimization:
PyoIterator::fold_starargs/kwargs truthiness are now matched to check if they are actually needed, and each case passes an optimized function toitertools::reduce. Without both for example, the method is was 1.2x faster. Once migrated to Rust, this case is now 2.23x faster. With args, the relative improvement is of 2.08x. With args AND kwargs, 1.87x. -
Rust migration and logic optimization:
PyoIterator::map_juxtis now fully in Rust as a "real"Iteratorinstead of aCallableused onbuiltin::map. Performance gains on small funcs tuples, performance regressions on large funcs tuples => 1.16x faster for 1 func, 1.08x for 4 funcs, 1.02x faster for 16 funcs, and 0.95x slower for 64 funcs. Optimizing the latter is on the roadmap, but all considered, I doubt most ppl will be usingmap_juxtwith more than 16 funcs, so It's acceptable in the meantime. -
Rust migration:
PyoIterator::onceis now 1.26x faster. -
Logic optimization:
PyoIterator::chaindoesn't callfrom_iterableneedlessly anymore. Unchanged on small iterators, but around 1.06x faster on 1k and 10k items when provided with a singleIterableargument. -
PyoSequence::{first, last, get}are now around 1.7x (Range) to 2.1x (Seq) faster, after having both the ABC and the concrete classes migrated to Rust.
PyoIterator::{all, any}with no predicate is now fully in Rust. unfortunately, a constant ~100 ns performance regression has been observed on benchmarks. On a relative basis, this is a 7 to 8% performance hit on small iterators(10 to 100 items). On 1000 items, a 2% hit, and on 10k it's barely noticeable (absolute time is 35 us for reference on 10k items).
- Various minor docstring improvements.
- Code readability and privacy management improvements in Rust source code.
PyoIterator::findhas been moved to Rust, performance is identical as before.
- Removed
pytest-covfrom dev dependencies.
- API change:
Inspectmixin is renamed toTap, and it'sinspectmethod is renamed totap. This aligns better with the Rust cratetapand it's traits, as well as avoiding confusion with theinspectmethod onResultandOptiontypes. - API change:
Intomixin is renamed toPipe, and it'sintomethod is renamed topipe. Same reasoning as forInspectmixin, and also aligns with polars, pandas and sqlglot API's. - API change:
Pipeablemixin is renamed toFluent, to avoid confusion withPipemixin. - API change:
SliceView::baseproperty is renamed toinner. - Return type:
PyoIterator::with_positionreturnedPositionvalues are now back totyping::Literalvalues, instead ofenum::StrEnuminstances. Unfortunately,Pyo3doesn't have good native support for python enums yet, and this would have required a lot of maintenance burden for something that doesn't really change the behavior of the method.
Iter::__bool__is now 1.16x faster by avoiding to useitertools::islice, this logic has been replaced by anextcall and sentinel object check.PyoIterator::with_positionhas been moved to Rust. 2.88x faster.
- typing: Widened the input types of various methods on set-like concrete classes (
Set,SetMut,StableSet, views classes).
- Fix: the check_nav script wasn't launching its
mainfunction. - Refactor: Use the
tapcrate troughout the Rust code for improved readability. - Refactor: replaced
unwrapcalls withexpector match cases in tools.rs for improved safety. - Various minor docstring improvements
- Added
tapcrate for Rust code.
- Bumped
Pyo3version to0.29.0.
This release main change is a refactor of Iter and PyoIterator methods -> almost all methods from Iter have been moved to PyoIterator.
Only a handful of them remain in Iter, mostly instance methods relying on direct manipulation of the inner iterator, as well as the dunder overrides for concrete implemetnation.
- API change:
Iter::filter_stardoes not accept arbitraryIterableas elements anymore, only tuples. - API change: The default
Seqcollector forIter::collecthas been deleted. All.collect()becomes.collect(Seq)if you want to keep the same behavior as before.
- no-copy: Avoid unecessary copy in
Vec::repeatby constructing the new instance by reference. Iter::filter_starhas been moved to Rust. 1.45x to 1.50x faster across benchmarks (tuples with 2, 3, 4, 5 elements).PyoIterator::{all, any}with predicate function migrated to Rust.1.02x to 1.05x faster.
- typing: Improved the input types of various
PyoSetmethods to align them with typeshed, as they were too strict. - typing:
Iter::{map_star, filter_star}can handle 1 element tuples now. - typing:
PyoIterable::iternow returns aPyoIteratorinstead of anIter, allowing more flexibility for custom overrides.
- Splitted check_nav and generate_docs logic in two separate scripts. Closes issue #15.
This release improve the performance of Iter::from_fn by:
- 1.35x with NO
args/kwargs(i.ef() -> Option[T]) - 1.5x with
argsORkwargs - 1.6x with
argsANDkwargs
This means that you can define inline generators/iterators in a new way, without much, if any, performance trade-off in respect to the other three python-native paths.
In the benchmarks, only generator comprehensions has a slight performance advantage over Iter::from_fn.
This is fine, because comprehensions should be the go-to solution whenever possible, as they are concise, idiomatic/pythonic, and fast.
The presence or absence of *args and **kwargs didn't have any substantial impact on the relative performance.
| Python way | Relative performance |
|---|---|
Iterator class with __iter__ and __next__ |
Identical |
function/method with yield statements |
Identical |
generator comprehension, i.e (x for x in iterable) |
0.9 to 0.95x (i.e from_fn is 5-10% slower) |
- Removed:
PyoMutableSequence::{into_iter, extend_move}. What led to this decision is the fact that Python nature made these methods very situational, as well as their poor speed.into_itercan be replaced byiter, andx.extend_move(y)byx.extend(y)followed byy.clear().
PyoMutableSequence::truncateis now 11x faster. The old implementation used an inefficient loop withpop()calls, while the new one usesdel self[length:], after double-checking that this is a no-copy operation.PyoMutableSequence::drainlogic has been improved to avoidpopcalls, which made it 6.5x faster. The subsequent Rust migration upped the improvement to 10.69x.PyoMutableSequence::extract_ifis the same story asdrain. 5.18x improvement from a logic change, upped to 8.28x once ported to Rust.Iter::successorsmigrated to Rust. 1.3x faster.
- Dev: Deleted unused script dependencies:
typer,polars.
- Refactor: Minor readability improvements in
scripts/.
- typing:
PyoIterator::sumhas been giving false positives for a while. This is now fixed, with the same inputs/outputs as the python builtin.
- Internal: Python dev version is by default 3.13 to avoid any unsupported patterns on our minimum supported version.
- Imports: Prioritizing
typing::TYPE_CHECKINGblocks import whenever possible. This could maybe speed-up initial import time.
-
Feat:
collections, new module mimicking pythoncollectionsmodule, aiming to provide additional, more specialized collections types. -
Feat:
collections::StableSet, a mutable collection of unique elements which remember their insertion order. Usesdict::fromkeysinternally, thus has the same characteristics as adictregarding lookup/insertion/deletion/iteration performance. -
Feat:
collections::Deque, pyochain version of python builtincollections::deque. -
Feat:
SliceView, a zero-copy, composable slice view over anycollections.abc.Sequence. It allows to create views into existing sequences without copying the data, and to compose them together in O(1) time. It also has anadvancemethod for shifting the view's window forward or backward in-place, which can be useful for sliding windows. Credits to @eirikurt for the implementation and the idea, as well as@hwelch-fle for the typing improvements work, which I used as base for the integration in pyochain.
Iter::map_withfor mapping multiple iterables at once, just likemapbuiltin when provided with multiple iterables.- Improved
__eq__forSet,SetMut, andDict, with similar behavior than their internal data structures, as well as pyochain objects handling. Set::__hash__, with similar behavior than it's internalfrozensetstructurerepeatmethod forSeqandVec, which call the__mul__dunder method.Vec::repeat_mut, equivalent tomy_list *= n.Iter::from_repeatfor repeating an object n times as elements of anIter.Dict::union_mut, similar todic_1 |= dic_2for in-place union of two dicts.Vec::into_iterfor consuming aVecinto anIterthat yield and pop each element from theVec.abc::{Into, Inspect}mixins, each providing one method from the originalPipeable. The latter inherit from both of them, thus no behavior change.abc::{PyoMutableSet, PyoSized, PyoReversible, PyoContainer}ABCs. This don't impact runtime nor existing API, but is used for the ABC hierarchy tree to better mimick pythoncollections.abc.PyoSet::is_superset_strictwas missing. Added.Dict::from_keys, equivalent todict.fromkeyspython builtin.
- Vec MRO:
Vecdoes not inherit fromSeqanymore, only fromPyoMutableSequence. This means that if you were relying onVecbeing a subclass ofSeqfor type checking or isinstance checks, you should now check againstPyoMutableSequenceinstead. - API change:
PyoIterator::{argmin, argmax}have been renamed toarg_minandarg_max, and theirkeyargument version is now in the form of a separate method,arg_min_byandarg_max_by. Just like the last releases with similar changes, adapt your code in consequence. - Removed:
PyoCollection::repeathas been removed. You can domy_collection.into(Iter.from_repeat, n)to get the exact same behavior. - API change:
Dict::mergehas been renamed tounion. - API change:
PyoIterable::{all, any, join, sum, min, my_by, max, max_by, all_unique_by, unpack_into}are moved toPyoIterator. Simply add a call toiter()in impacted code if aPyoCollectionwas used. - API change:
PyoIterable::lengthhas been removed.PyoIterator::countandPyoSized::lenare their replacement. Closer to Rust semantics, and do things more explicitely, as the former is a full iteration that count the elements, while the latter is a call tolen(). - API change: All the methods that have been moved from
PyoIteratortoIterin the 0.20.0 release are now back in their original ABC. To handle this, a new_from_iterableprivate method has been added, the idea being identical to what python stdlib does with set ABCs. See the method documentation for more details. - API change:
Iter::with_positionnow yieldPosition(StrEnum)values instead of literal strings. - Removed:
PyoIterable::all_uniquehas been removed and is now only onPyoIterator. If you used it on aPyoCollection, you can either add a call toiter()before, or if you want to keep the exact same underlying implementation, compare the length of the collection with the length of aSetcreated from it. Examples and explanations in the documentation ofall_uniquemethod. - Removed:
PyoIterable::second. Use 2 calls toitertator.next()orsequence[1]instead.
- Moved to 🦀 ->
Iter::filter_map. 1.15x (64 elements) to 1.25x (256, 1024, 4096 elements) faster. - Moved to 🦀 ->
Iter::filter_map_star. 1.24x (64 elements) to 1.38x (256, 1024, 4096 elements) faster. - Moved to 🦀 ->
Iter::scan. More or less 1.35x faster across sizes (64, 256, 1024, 4096, 16384 elements). - Moved to 🦀 ->
Iter::map_while. 1.38x to 1.44x faster across sizes. - tail no-copy:
PyoIterator::tailcreated internally adequewho was then re-wrapped in aSeqfor the return. It now directly return a pyochainDequecreated by reference from the aforementionneddeque.
- typing:
Iter::{filter, filter_false}now handle type narrowing forT | NoneandTypeGuardwhen possible. - API:
Iter::{filter, filter_false}provided closure is now optional, just like python builtins. - API:
Iter::{partition, collect}has been moved toPyoIterator, meaning all user-defined iterators can now call them. - API:
Iter::from_fnnow accepts*argsand**kwargsto be passed to the generator function. Inputs are matched to minimize overhead and keep original performance if no additional arguments are needed.
- Correctly handle hyperlinks in
See Alsosections. WIP to do the same for the rest of the sections. - Various docstrings changes with better explanations and examples.
- Fix: Added
maturinto pyproject.toml as a dev dependency.
- Added
--previewoption toRuffformat workflow to garantuee markdown formatting - Reorganized the
abcsubmodule by splitting the monolithic_iterable.pyfile into multiple ones with more specific content. - Moveed the views objects to a dedicated file for readability.
- Improved the doc generator script redability and it's handling of submodules.
- API:
Vec::sortis split intoVec::sortandVec::sort_by. If you were usingVec::sort(key=...), you should now useVec::sort_by(key=...)instead. This aligns it withPyoIterator::{sort, sort_by}change from last release.
- Feat: Added a clearer repr for
Rangethat shows the start, stop and step values. - Feat:
Vechave an__eq__method. expect same results thanlist::__eq__, except that it also works with otherVecinstances. - Feat:
Seqalso now have an__eq__method. Same story asVec. It also has an__hash__method, which is the same astuple::__hash__, and is compatible with it, meaning that aSeqand atuplewith the same content will have the same hash.
- Less code duplication in scripts with a common module
- New pytest plugin improving docstring/markdown testing
- Separated
Ruffconfig in a dedicated toml file
- Various improvements in docstrings, regarding formatting for the website, additional examples, etc..
- Removed:
Set::r_intersection, r_union, r_difference, r_symmetric_difference-> these methods were redundant and not really useful in practice, as the same behavior can be achieved by swapping the order of the sets in the non-reverse methods. For example,a.r_intersection(b)is equivalent tob.intersection(a), and so on for the other methods. - Migrated:
PyoKeysView,PyoValuesViewandPyoItemsVieware moved fromabcsubmodule to surface-level API. If you previously imported them asfrom pyochain.abc, update your imports tofrom pyochaininstead.
PyoKeysViewandPyoItemsViewlost their set-like methodsunion,intersection, etc... in the last release. It was an overlook of the removal of these methods from thePyoSetABC. This is now fixed.
- Better returns: Due to the aforementioned bugfix, I took the opportunity to double check what was really returned from those operations before being wrapped in a
self::__class__call. Turns out that by default, keys and item views were returningsetobjects, which were immediately re-wrapped in their callers, which may have entailed unecessary copies, but most importantly the API loss of an already constructed object with more methods (SetMut). Now, each class (PyoKeysView,PyoItemsView,Set,SetMut) has a dedicated implementation of these methods, which garantee no-copy behavior with a better API.
- API change:
pyochain::traitsmodule is nowpyochain::abc. If you were importing frompyochain.traits, update your imports topyochain.abcinstead. - Removed:
PyoIterable::new. Call__init__(())for the same behavior, e.gSeq(),Iter(), etc... - Removed:
UnzippedandPeekabledataclasses. TheItermethods who constructed them now simply return tuples instead, simplifying the API and improving speed. - API change:
Iter::sort(now in base classPyoIterator) has been split intoIter::sortandIter::sort_by. If you were usingIter::sort(key=...), you should now useIter::sort_by(key=...)instead. This should bring typing improvements as well as a clearer API. - API change:
PyoIterable::__init__is deleted. This means that subclasses are free to implement their own constructors, without typing constraints nor default behavior.
If you did not define custom classes from PyoSet or PyoIterator, skip to the Enhancements section.
The __init__ deletion from PyoIterable also means that all ABCs methods that relied on self::__class__ needed to move to their concrete pyochain parents, as they couldn't stay (nor should have ever been) purely abstract.
This concerns:
from PyoSet to Set ->
intersection, r_intersection, union, r_union, difference, r_difference, symmetric_difference, r_symmetric_difference
from PyoIterator to Iter ->
take_while, skip_while, compress, unique, unique_by, take, skip, step_by, slice, cycle, insert, intersperse, chain, accumulate
- Migrated:
Iter::{collect_into, try_collect, sort, tail}have been moved toPyoIterator, meaning all user-defined subclasses can now call them. - Migrated:
Vec::{drain, extract_if}have been moved toPyoMutableSequence, meaning all user-defined mutable sequence subclasses can now call them.
- Improved various classes and methods documentation.
- Reformatted all code examples with Ruff.
- Added dev documentation regarding design choices on where to implement a method (abstract vs concrete class).
- Separated some classes in dedicated files to reduce the size of
_iter.pyand improve readability.Seqis now in_seq.py,SetandSetMutare now in_set.pynotably.
- dependency-free: pyochain is now dependency free and do not need
cytoolzanymore. All the methods that were using it have been reimplemented in Rust or removed. See the breaking changes and performance improvements/regressions sections for more details.
- API change:
Iter::try_for_eachnow returnsResult[tuple[()], E]instead ofResult[None, E]. - API change:
PyoIterator::uniqueis split intoIter::uniqueandIter::unique_by. If you were usingIter::unique(key=...), you should now useIter::unique_by(key=...)instead.
Just like last release, some methods have beeen removed for a leaner, simpler API and a quicker no-dependency migration path.
See the table below for the removed methods and their recommended alternatives if you need the same behavior:
| Method name | Equivalent | Notes |
|---|---|---|
Iter::diff_at |
cytoolz::itertoolz::diff |
- |
Iter::is_strictly_n |
more_itertools::strictly_n |
- |
Iter::top_n |
cytoolz::itertoolz::topk |
- |
PyoIterator::random_sample |
cytoolz::itertoolz::random_sample |
- |
PyoIterator::interleave |
cytoolz::itertoolz::interleave |
Unpack self and the other iterables in a single one before calling the function. |
Vec::concat_mutfor in-place concatenation of anotherVecorlistSeq::concatfor concatenation of anotherSeqortuple.
PyoMutableSequence::extend_movedoesn't usefunctools::partialinternally anymore. Expect some very light performance improvements.
Various methods have been migrated to Rust, from Python or Cython.
See the table below for the performance (2x means 2 times faster, 0.5x means 2 times slower).
Unfortunately, 3 methods have seen a performance regression vs the old Cython implementation, see the next section for details.
| Method name | From | Improvement | Notes |
|---|---|---|---|
Iter::try_for_each |
Python | 4.6 - 4.7x | - |
Iter::try_collect |
Python | 2.3 - 3x | - |
PyoMutableSequence::retain |
Python | 1.35 - 1.4x | - |
Iter::{map_windows, map_windows_star} |
Cython | n=32: 1.17x, n=128: 1.40x | Slower for smaller window sizes (0.81x for n=2, 0.93x for n=8). |
Iter::map_juxt |
Cython | 1.2x to 1.5x | Slower for a single func (0.95x), but not useful in practice, use Iter::map instead |
The 3 following methods have been migrated from Cython to Rust, and have unfortunately seen a performance regression compared to their old Cython counterparts.
However, they are still much faster than a pure Python implementation.
See the table below for the details.
| Method name | Vs Cython | VS Python | Notes |
|---|---|---|---|
PyoIterator::unique_by |
0.95 | 2.31x | Equivalent to old PyoIterator::unique(key=...) |
PyoIterator::unique |
0.75 | 7.3X | - |
PyoIterator::intersperse |
0.60 | 4.95x | - |
- typing:
Some(NONE)is directly inferred asOption[T], lowering the number of potential errors. - typing:
Iter::try_collecthas received new overloads and should be inferred more accurately now. - typing:
Iter::map_juxthas received new overloads to precisely infer the return type up to 10 functions.
PyoIterator::{last, length}were not properly handling custom Iterators and StopIteration errors. Fixed.
- Renamed
converters.rsfile in Rust tomixins.rsto better reflect its content and purpose. - Internal: Renamed
PyOkandPyErrtoPyoOkandPyoErrto avoid any confusion withPyO3types. - Internal: Various renaming and cosmetic changes in Rust for better readability and consistency.
- Various reorganisation changes to reduce redundancy
- Updated CONTRIBUTING.md with latest architecture status
- Improved docstrings of various classes and methods with better examples and explanations
- Fix:
PyoIterator::for_each_starwas wrongly typed as working with anIteratorcontaining anyIterableelements, instead of onlytuples.
- standardized iteration sizes across benchmarks over iterators/collections
- Various internal improvements to scripts for code readability
- Added checks in
scripts/check_doscstringsto flag invalid existing links in zensical navigation, README and CONTRIBUTING.md, to prevent future broken links from being merged.
- Added
sdsortas a dev dependency for code formatting and sorting.
- Removed:
PyoIterator::is_distinctwas doing the exact same thing asPyoIterator::all_uniquewithout a key function. - Removed:
Iter::{partition_all, partition_by}. Call the corresponding functions incytoolz.itertoolzinstead. - Removed:
Iter::unique_to_each. It was acting like it was lazy, but it was immediately consuming the whole iterator and storing the results in memory, as well as being niche. If you need this behavior, use themore_itertoolcorresponding function instead. - Removed:
PyoIterator::elements-> this was not lazy, but the API acted like it was. If you need this behavior, callcollections::Counter::elementsinstead. - Removed:
Vec::most_common-> simple wrapper aroundcollections::Counter::most_common. Call it directly instead if you need it. - Removed:
Iter::{split_before, split_after, split_at, split_into, split_when}. Call the corresponding functions inmore_itertoolsinstead. - Behavior change:
PyoIterator::partitionnow return a tuple ofVecfrom a predicate function, instead of anIteroftuplesof length n. It aligns with the Rust implementation, and is often more useful in practice. Call the corresponding function incytoolz.itertoolzinstead if you need the old behavior. - API change:
PyoIterator::all_uniquedoes not accept a key function anymore. If you need one, callall_unique_byinstead.
- Check safety: Guarantee singleton behavior for
Null. CallingNull()will always return the same instance, which isNONE. This allows for identity checks (is) to work as expected withNull, and ensures that there are no multiple instances ofNullfloating around in the system. This also means that you can useNull()instead ofNONEif you prefer, without worrying about breaking the singleton property. - Feat:
PyoIterator::all_uniqueis migrated toPyoIterable, meaning ALL collections now can call it without converting to an iterator first.PyoSequenceandPyoSet(and by extensionSeq,Set, etc...) have their own optimized implementations - Feat: Added
PyoIterable::all_unique_byfor checking uniqueness based on a custom key function. This is the same as formerall_unique(key=...), but with a clearer name and intent.
- Using
cast_exact/is_exact_instance_ofinstead ofcast/is_instance_ofwhen interacting with pyochain types (Result/Option) in Rust methods bring an overall +2% to +5% performance gain across benchmarks. VariousOption/Resultmethods, as well aIter::{try_reduce, try_find, try_fold}, benefit from this change. all_uniquenow call cython level code and is has fast as the deprecatedis_distinctmethod.PyoIterable::all_unique_by(oldPyoIterator::all_unique(key=...)path) is now in Rust, and is 30 (100 elements) to 70% (500 elements) faster than the old implementation. Larger collections should see even better improvements.PyoIterable::all_uniqueis in Rust as well, with a slight performance regression of around 1 to 5% compared to the oldis_distinctmethod. Note that if you were usingall_equalinstead, you should expect performance improvement instead, in the same ballpark asall_unique_by.
- Renamed
PyNonetoPyNullin Rust to avoid confusion with Pyo3 types. - Various cosmetic changes in Rust to improve readability and documentation
- Extracted
PyoIterable::{first, second},PyoIterator::{chain, insert, step_by},Iter::tailfrom cytoolz to python, as they were trivial function calls who didn't needed any external dependency work. - Extracted
PyoIterable::{last, length}from cytoolz to Rust. No performance regression.
- Replaced
cytoolzcalls inIter::skip. Slight performance regression, butcytoolzwasn't creating a new Iterator, but was eagerly consuming the original one until n elements were skipped, which could be a problem with large n/collections, and could cause issues if lazyness was expected. The new implementation usesitertools.islice, which creates a new iterator that skips the first n elements without consuming the original one.
- Benchmarks -> deletions, renaming and new ones
- Cleaned up unneded/redundants tests for args concatenation and Result
- Docstring checker was broken and not flagging uncorrect docstrings. Fixed.
- Small improvements in
IterandPyoIterabledocstrings
The Option type has been redesigned to be more consistent with Result, with a new "dummy" Protocol OptionType as the base class for Some and NONE.
This change allows pattern matching exhaustiveness checks and much simpler internal Rust implementation, since now Option is, just like Result, a type union from the POV of type checkers. In Rust, it's an empty struct. No performance impact has been observed.
The following methods have been removed or renamed:
Option::__init__=>option(pure function)Option::if_true=>then_if_true(pure function)Option::if_some=>then_if_some(pure function)
- New template sections in CONTRIBUTING.md for standardizing the CHANGELOG and releases
- Caller sites of
Option::__init__have been updated
- Various new benchmarks to cover impacted methods from the breaking changes
Iter::for_each_starnow handleargsandkwargsin the same way asIter::for_each, allowing to pass arguments to the function being called for each item.
- Migrated
Iter::for_each, for_each_starto Rust. 1.5x-2x faster in average. Rangedid not have__slots__properly set. Fixed.
- New traits in
rust::typesto improve the readability when handling Callable arguments with args and kwargs. instead of call(func, self, args, kwargs), we can now do func.call(self, args, kwargs). - Internal scripts cosmetic changes to standardize them with recommended pattern ->
from pyochain import xinstead ofimport pyochain as pc
- Improvements to
traits::PyoSetmethods documentation, with better examples and explanations. - Updated documentation to reflect the new pattern ->
from pyochain import xinstead ofimport pyochain as pc - Various other minor improvements and fixes.
pytest-benchmarksetup, used and now part of the developpement process to catch performance regressions.- Various other minor improvements and fixes.