Lightweight functional-style helpers for Python 3.13+
Project description
fnutil
Lightweight functional-style helpers for Python 3.13+.
Four composable primitives:
Expression[T]— wraps a single value in a chainable, Option-aware container with arithmetic and comparison operators.Iterator[T]— wraps any iterable with a lazy, chainable adapter API modelled on Rust'sIteratortrait.Result[T, E]— represents either success (Ok[T]) or failure (Err[E]), modelled on Rust'sResulttype.Lazy[T]— defers evaluation of a zero-argument callable until the value is first accessed.
Installation
pip install fnutil
uv add fnutil
Quick start
from fnutil import expr, it, ok, err, lazy
# Expression: wrap → transform → unwrap
result = (
expr(10)
.map(lambda x: x * 2)
.filter(lambda x: x > 15)
.unwrap_or(0)
)
assert result == 20
# Iterator: wrap → chain lazy adapters → collect
total = (
it(range(10))
.filter(lambda x: x % 2 == 0)
.map(lambda x: x ** 2)
.fold(0, lambda acc, x: acc + x)
)
assert total == 120
# Result: explicit success / failure without exceptions
def parse_int(s: str):
return ok(int(s)) if s.isdigit() else err(f"not a number: {s!r}")
assert parse_int("42").map(lambda x: x * 2).unwrap_or(0) == 84
assert parse_int("abc").map(lambda x: x * 2).unwrap_or(0) == 0
# Lazy: defer expensive work until needed
import time
slow = lazy(lambda: time.sleep(0) or "done")
assert slow.is_evaluated is False
assert slow.value == "done"
assert slow.is_evaluated is True
Expression[T]
from fnutil import expr
Construction
e = expr(42) # Expression[int]
e = expr(None) # Expression[None]
e = expr([1, 2, 3]) # Expression[list[int]]
Transform
| Method | Description |
|---|---|
.map(fn) |
Apply fn to the value; return Expression[U] |
.flat_map(fn) |
Apply fn which returns Expression[U]; unwrap one level |
.pipe(f, g, h) |
Thread value through functions left-to-right |
.then(fn) |
Apply fn and return the raw result (exits the container) |
.inspect(fn) |
Call fn for side-effects; return self unchanged |
.zip(other) |
Pair with another Expression; return Expression[tuple[T, U]] |
.flatten() |
Unwrap Expression[Expression[U]] → Expression[U] |
.try_map(fn) |
Apply fn; return Ok(result) on success or Err(exc) on exception |
Option-like
| Method / Property | Description |
|---|---|
.is_some |
True when value is not None |
.is_none |
True when value is None |
.filter(fn) |
Return self if fn(value) is truthy, else Expression(None) |
.or_else(default) |
Return self when is_some, else Expression(default) |
.or_else_with(fn) |
Return self when is_some, else Expression(fn()) |
.unwrap_or(default) |
Return inner value when is_some, else default (raw) |
Iterator interop
expr([1, 2, 3]).iter() # → Iterator[int]
expr(range(5)).iter() # → Iterator[int]
Operators
Arithmetic and comparison operators delegate to the inner value. Both operands must be Expression instances.
expr(3) + expr(4) # Expression(7)
expr(10) / expr(4) # Expression(2.5)
expr(2) ** expr(8) # Expression(256)
-expr(5) # Expression(-5)
abs(expr(-7)) # Expression(7)
expr(1) < expr(2) # True
expr(3) >= expr(3) # True
sorted([expr(3), expr(1), expr(2)]) # [Expression(1), Expression(2), Expression(3)]
Equality
Expression equality compares inner values. Comparing with a raw value returns False.
expr(1) == expr(1) # True
expr(1) == 1 # False
Iterator[T]
from fnutil import it
Construction
it([1, 2, 3])
it(range(10))
it(x for x in some_generator())
# Factory methods on `it`
it.repeat(0, times=3) # → Iterator yielding 0, 0, 0
it.repeat(1) # → infinite Iterator of 1s (use .take(n))
it.count(start=0, step=1) # → infinite 0, 1, 2, …
it.empty() # → empty Iterator[T]
Lazy adapters (return Iterator)
| Method | Description |
|---|---|
.filter(fn) |
Keep elements where fn is truthy |
.map(fn) |
Transform each element |
.enumerate() |
Yield (index, element) tuples |
.zip(other) |
Zip with another iterable (truncates to shortest) |
.zip_longest(other, fillvalue=None) |
Zip, padding the shorter side |
.chain(other) |
Concatenate another iterable |
.take(n) |
First n elements |
.skip(n) |
Skip first n elements |
.take_while(fn) |
Take elements while fn is truthy |
.skip_while(fn) |
Skip elements while fn is truthy |
.flat_map(fn) |
Map fn then flatten one level |
.flatten() |
Flatten one level of nesting |
.inspect(fn) |
Side-effect on each element; pass through unchanged |
.filter_false(fn) |
Keep elements where fn is falsy |
.accumulate(fn=None, *, initial=None) |
Running accumulated values |
.cycle() |
Repeat endlessly |
.batched(n) |
Non-overlapping tuples of length n |
.pairwise() |
Successive overlapping pairs |
.compress(selectors) |
Keep elements whose selector is truthy |
.starmap(fn) |
Unpack each element as fn(*item) |
.group_by(fn=None) |
Group consecutive elements by key |
[i] / [start:stop:step] |
Integer index (take) or slice (islice) |
Consuming terminators
| Method | Returns | Description |
|---|---|---|
.collect(factory) |
U |
e.g. .collect(list), .collect(set) |
.collect_expr(factory) |
Expression[U] |
Same, wrapped in Expression |
.fold(init, fn) |
U |
Left-fold with initial value |
.reduce(fn) |
T | None |
Left-fold without init; None on empty |
.for_each(fn) |
None |
Consume, calling fn on each element |
.count() |
int |
Number of elements |
.sum() |
T |
Sum of all elements |
.product() |
T |
Product of all elements |
.unzip() |
(list, list) |
Split an iterator of pairs into two lists |
.find(fn) |
T | None |
First element where fn is truthy |
.any(fn) |
bool |
True if any element satisfies fn |
.all(fn) |
bool |
True if all elements satisfy fn |
.min() |
T | None |
Minimum element; None if empty |
.max() |
T | None |
Maximum element; None if empty |
.nth(n) |
T | None |
Element at zero-based index n; None if out of range |
.last() |
T | None |
Last element; None if empty |
.partition(fn) |
(list[T], list[T]) |
(truthy, falsy) split |
.value |
Iterable[T] |
Raw underlying iterable |
Examples
from fnutil import it
it([1, 2, 3]).map(lambda x: x * 2).collect(list) # [2, 4, 6]
it(range(5)).sum() # 10
it(range(1, 6)).product() # 120
it([(1, "a"), (2, "b")]).unzip() # ([1, 2], ['a', 'b'])
it.count(step=2).take(4).collect(list) # [0, 2, 4, 6]
# Group consecutive words by first letter
words = ["ant", "bee", "bat", "cat"]
for letter, group in it(sorted(words)).group_by(lambda w: w[0]):
print(letter, group.collect(list))
# a ['ant']
# b ['bat', 'bee']
# c ['cat']
# Batch into chunks
it(range(7)).batched(3).collect(list)
# [(0, 1, 2), (3, 4, 5), (6,)]
Result[T, E]
from fnutil import ok, err, Ok, Err, Result
Represents either a successful value (Ok[T]) or a failure (Err[E]). Modelled on Rust's Result type.
Construction
ok(42) # Ok[int]
err("oops") # Err[str]
Ok(42) # same as ok(42)
Err("oops") # same as err("oops")
Properties
| Property | Description |
|---|---|
.is_ok |
True for Ok values |
.is_err |
True for Err values |
For Ok instances: .value returns the inner value.
For Err instances: .error returns the inner error.
Methods
| Method | Description |
|---|---|
.map(fn) |
Apply fn to the value if Ok; pass Err through unchanged |
.map_err(fn) |
Apply fn to the error if Err; pass Ok through unchanged |
.flat_map(fn) |
Apply fn (which returns a Result) if Ok; pass Err through |
.inspect(fn) |
Call fn for side-effects if Ok; return self |
.inspect_err(fn) |
Call fn for side-effects if Err; return self |
.unwrap_or(default) |
Return value if Ok, else default |
.unwrap_or_else(fn) |
Return value if Ok, else fn(error) |
.unwrap() |
Return value if Ok, else raise ValueError |
.expect(msg) |
Return value if Ok, else raise ValueError(msg) |
.ok() |
Return Expression[T | None] — value if Ok, else None |
.err_value() |
Return Expression[E | None] — error if Err, else None |
Examples
from fnutil import ok, err
# Pipeline — errors short-circuit
result = (
ok("42")
.map(int)
.map(lambda x: x * 2)
.unwrap_or(0)
)
assert result == 84
result = (
err("bad input")
.map(int) # skipped
.map(lambda x: x * 2) # skipped
.unwrap_or(0)
)
assert result == 0
# isinstance checks
assert isinstance(ok(1), Ok)
assert isinstance(err("x"), Err)
assert isinstance(ok(1), Result)
# Convert to Expression
ok(10).ok() # Expression(10)
err("x").ok() # Expression(None)
err("x").err_value() # Expression('x')
Lazy[T]
from fnutil import lazy, Lazy
Defers evaluation of a zero-argument callable until .value or .force() is first accessed. The result is cached — the callable is invoked at most once.
Construction
lz = lazy(lambda: expensive_computation())
lz = Lazy(lambda: expensive_computation())
Properties and methods
| Method / Property | Description |
|---|---|
.is_evaluated |
True after the value has been computed |
.value |
Compute (if needed) and return the value |
.force() |
Alias for .value |
.map(fn) |
Return a new Lazy that applies fn to the value when forced |
.flat_map(fn) |
Return a new Lazy that applies fn (→ Lazy[U]) and forces it |
.pipe(f, g, h) |
Return a new Lazy that threads the value through functions |
.expr() |
Force the value and wrap it in an Expression |
Examples
from fnutil import lazy
calls = []
lz = lazy(lambda: calls.append(1) or 42)
assert lz.is_evaluated is False
assert lz.value == 42 # evaluated here
assert lz.value == 42 # cached — callable not called again
assert len(calls) == 1
# Lazy transformation pipeline (nothing runs until .force())
result = (
lazy(lambda: 10)
.map(lambda x: x * 2)
.map(lambda x: x + 5)
.force()
)
assert result == 25
Type checking
fnutil ships a py.typed marker and .pyi stub files for all modules. It is fully compatible with Pyright and mypy.
Requirements
- Python 3.13+
- No runtime dependencies
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fnutil-1.1.0.tar.gz.
File metadata
- Download URL: fnutil-1.1.0.tar.gz
- Upload date:
- Size: 11.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.22
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e4f3590cb72c13618eb189bb47392d02c19493f543243cf2d2b6888493e519c9
|
|
| MD5 |
8d31831890c218a0fb3e7f60dcd3ef49
|
|
| BLAKE2b-256 |
ea003825c6102a23f76ad33b0613a74d2e896615657cb7eee27512d049accb16
|
File details
Details for the file fnutil-1.1.0-py3-none-any.whl.
File metadata
- Download URL: fnutil-1.1.0-py3-none-any.whl
- Upload date:
- Size: 15.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.22
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3dc02d6f0773a44f2f941213d7aa7a491bbdc33e7195a2c33e147d79ec7fdcb
|
|
| MD5 |
0ce824ec8a62e1c42d2b84ae2140def0
|
|
| BLAKE2b-256 |
65a79c0d423234173ce063c28a3f822c5dd2bf0e68000fec9a978338d61692f1
|