Skip to main content

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's Iterator trait.
  • Result[T, E] — represents either success (Ok[T]) or failure (Err[E]), modelled on Rust's Result type.
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fnutil-1.1.0.tar.gz (11.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fnutil-1.1.0-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

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

Hashes for fnutil-1.1.0.tar.gz
Algorithm Hash digest
SHA256 e4f3590cb72c13618eb189bb47392d02c19493f543243cf2d2b6888493e519c9
MD5 8d31831890c218a0fb3e7f60dcd3ef49
BLAKE2b-256 ea003825c6102a23f76ad33b0613a74d2e896615657cb7eee27512d049accb16

See more details on using hashes here.

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

Hashes for fnutil-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d3dc02d6f0773a44f2f941213d7aa7a491bbdc33e7195a2c33e147d79ec7fdcb
MD5 0ce824ec8a62e1c42d2b84ae2140def0
BLAKE2b-256 65a79c0d423234173ce063c28a3f822c5dd2bf0e68000fec9a978338d61692f1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page