Functional-style method chaining for Python data structures.
pyochain brings a fluent, declarative API inspired by Rust's Iterator, Result, Option, and DataFrame libraries like Polars to your everyday Python iterables and dictionaries.
Manipulate data through composable chains of operations and manage errors and optional values safely, all while enjoying type-safe guarantees.
uv add pyochainProvides the following core classes and utilities:
Iter[T]- A superset of Python
collections.abc.Iterator, with chainable functional methods. - Underlying data structure is an
iterator(if we can call it that). - Implement
IteratorProtocol. - Can be converted to
Seq[T]|Vec[T]|Set[T]|SetMut[T]with the.collect()method (default to Seq[T]).
- A superset of Python
Seq[T]- An immutable collection with chainable methods.
- Underlying data structure is a
tuple. - Can be converted to
Iterwith the.iter()method. - Implement
SequenceProtocol.
Vec[T]- A mutable collection with chainable methods.
- Underlying data structure is a
list. - Can be converted to
Iterwith the.iter()method. - Implement
MutableSequenceProtocol.
Set[T]- An immutable, unordered collection of unique elements with chainable methods.
- Underlying data structure is a
frozenset. - Can be converted to
Iterwith the.iter()method. - Implement
SetProtocol.
SetMut[T]- A mutable, unordered collection of unique elements with chainable methods.
- Underlying data structure is a
set. - Can be converted to
Iterwith the.iter()method. - Implement
MutableSetProtocol.
Dict[K, V]- A mutable mapping with chainable methods.
- Underlying data structure is a
dict. - Implement
MutableMappingProtocol. - Can be converted to
Iterwith the.iter(),.keys_iter(),.values_iter()methods.
Option[T] | Some[T] | NONE- A type representing an optional value.
- Provides all methods from the Rust stdlib
OptionTrait (as long as they are applicable/made sense in a Python context). - Analog to a superset of
T | None. - Can be converted to
Iter[T]with the.iter()method. - Can be converted to Result with the
.ok_or()method.
Result[T, E] | Ok[T] | Err[E]- A type representing either a success (
Ok[T]) or failure (Err[E]), similar to Rust'sResultEnum. - Provides all methods from the Rust stdlib
ResultTrait (as long as they are applicable/made sense in a Python context). - Analog to a superset of
T | Exception. - Can be converted to
Iter[T]with the.iter()method. - Can be converted to
Option[T]with the.ok()method.
- A type representing either a success (
All provided classes share the following core methods for enhanced usability:
-
.inspect(): Insert functions who compute side-effects in the chain without breaking it (print, mutation of an external variable, logging...). If Option or Result, call the function only ifSomeorOk. -
.into(): Take aCallable[[Self, P], T]as argument to convert from Self to T in a chained way. E.gSeq[T].into()can take any function/object that expect aSequence[T]as argument, and return it's resultR. Conceptually, replacef(x, args, kwargs)withx.into(f, args, kwargs).
The full API reference can be found at: https://outsquarecapital.github.io/pyochain/
pyochain is currently in early development (< 1.0), and the API may undergo significant changes multiple times before reaching a stable 1.0 release.
>>> import pyochain as pc
>>>
>>> result = (
... pc.Iter.from_count(1) # Infinite iterator: 1, 2, 3, ...
... .filter(lambda x: x % 2 != 0) # Keep odd numbers
... .map(lambda x: x * x) # Square them
... .take(5) # Take the first 5
... .collect() # Materialize the result into a Seq
... )
>>> result
Seq(1, 9, 25, 49, 81)Write robust code by handling potential failures explicitly.
>>> import pyochain as pc
>>>
>>> def divide(a: int, b: int) -> pc.Result[float, str]:
... if b == 0:
... return pc.Err("Cannot divide by zero")
... return pc.Ok(a / b)
>>>
>>> # --- With Result ---
>>> res1 = divide(10, 2)
>>> res1
Ok(5.0)
>>> res2 = divide(10, 0)
>>> res2
Err('Cannot divide by zero')
>>> # Safely unwrap or provide a default
>>> res2.unwrap_or(0.0)
0.0
>>> # Map over a successful result
>>> res1.map(lambda x: x * x)
Ok(25.0)
>>> # --- With Option ---
>>> def find_user(user_id: int) -> pc.Option[str]:
... users = {1: "Alice", 2: "Bob"}
... return pc.Some(users.get(user_id)) if user_id in users else pc.NONE
>>>
>>> find_user(1).map(str.upper).unwrap_or("Not Found")
'ALICE'
>>> find_user(3).unwrap_or("Not Found")
'Not Found'Find more examples in the examples file.
- Declarative over Imperative: Replace explicit
forandwhileloops with sequences of high-level operations (map, filter, group, join...). - Fluent Chaining: Most methods transforms the data and returns a new wrapper instance, allowing for seamless chaining.
- Lazy first: All methods on collections that use an Iterator (think most for loop) and do not need to materialize data immediately are in
Iter[T]. Only methods that directly returns booleans, single values, or need to operate on the whole dataset (sorting for example) are shared between theIterableclasses (Seq,Vec,Set,SetMut) via their common base class. This encourages the use of lazy processing by default (since you have to explicitly calliter()to get access to most methods), and collecting only at the last possible moment. - Explicit mutability:
Seqis the usual return type for most methods who materialize data, hence improving memory efficiency and safety, compared to using list everytime.Vecis provided when mutability is required. Same forSetandSetMut. In python, set is the "default" set type (constructor and shorter name), but in pyochain Set is a frozenset. - 100% Type-safe: Extensive use of generics and overloads ensures type safety and improves developer experience. The library is fully typed and autocompletion is a central concern.
- Documentation-first: Each method is thoroughly documented with clear explanations, and usage examples. Before any commit is made, each docstring is automatically tested to ensure accuracy. This also allows for a convenient experience in IDEs, where developers can easily access documentation with a simple hover of the mouse, with a guarantee that the examples work as intended.
- Functional and chained paradigm: Design encourages building complex data transformations by composing simple, reusable functions on known buildings blocks, rather than implementing customs classes each time.
- Rust's language and Rust stdlib: Emulate naming conventions (
into()) and leverage concepts from Rust's powerful iterator traits (method chaining, lazy evaluation), Option and Result enums, to bring similar expressiveness to Python. - Python iterators libraries: Libraries like
cytoolz, andmore-itertoolsprovided ideas, inspiration, and implementations for many of the iterator methods. - PyFunctional: Although not directly used (because I started writing pyochain before discovering it), also shares similar goals and ideas.
Most of the computations are done with implementations from the cytoolz and more-itertools libraries.
An extensive use of the itertools stdlib module is also to be noted.
pyochain acts as a unifying API layer over these powerful tools.
https://github.com/pytoolz/cytoolz
https://github.com/more-itertools/more-itertools
The stubs used for the developpement, made by the maintainer of pyochain, can be found here: