Skip to content

Repository files navigation

dizzle

dizzle.png

Rust-powered functional toolkit for Python 3.13+: the toolz API you know, an Option/Result you've wanted, and a fluent Iter — all executing in native code.

Install

pip install dizzle  # or: uv add dizzle

The three layers

# 1. toolz-compatible flat functions — switch by changing an import
from dizzle import groupby, frequencies, merge, pipe, curried

groupby(len, ["a", "bb", "cc", "d"])   # {1: ['a', 'd'], 2: ['bb', 'cc']}

# 2. Option/Result — partiality without try/except
from dizzle.option import Option, get_in_opt, Some

loc = (get_in_opt(payload, "results", 0, "geometry", "location")
       .map(Location.from_dict)
       .unwrap_or(None))

match Option.of(user.email):
    case Some(email): send(email)
    case _:           skip()

# 3. Fluent Iter — Rust-iterator ergonomics, single-pass, lazy
from dizzle import Iter

(Iter(rows)
    .filter(lambda r: r["active"])
    .pluck("city")
    .frequencies())

Semantics

  • Flat layer matches toolz exactly — including exceptions (first([]) raises).
  • Option-returning twins (first_opt, get_in_opt, …) and all Iter partial terminals never raise on missing data.
  • Your callbacks' exceptions always propagate — nothing is swallowed.

Development

uv sync                      # builds the Rust extension via maturin
uv run pytest                # oracle + property tests against toolz
uv run pytest benches --benchmark-only   # vs cytoolz

Performance

Informational benchmarks against cytoolz and toolz live in benches/ and run explicitly:

uv run pytest benches --benchmark-only

As of 2026-08-27 (Python 3.13, Apple Silicon; median mean-time of 3 runs, ratio = dizzle/cytoolz, lower is better). The five benchmarks of the 0.4.0 suite hold at geometric mean 0.79x (~21% faster than cytoolz); the broad 16-group suite of 0.5.0, deliberately including the worst cases, sits at 1.07x; the two int64-buffer groups added in 0.6.0 land at 0.07x / 0.10x (10–14x faster), putting the full 18-group suite at 0.81x:

benchmark dizzle cytoolz toolz vs cytoolz
frequencies (30k int64 buffer) 52 us 756 us 1164 us 0.07x
unique (30k int64 buffer) 36 us 363 us 414 us 0.10x
frequencies (30k strs) 364 us 709 us 1091 us 0.51x
curry 3-arg chain 5 us 10 us 13 us 0.54x
groupby(len) (30k strs) 318 us 484 us 494 us 0.66x
unique (30k ints) 149 us 195 us 393 us 0.76x
take(1000) (30k ints) 5 us 5 us 5 us 1.00x (tie)
drop(29k) (30k ints) 64 us 63 us 65 us 1.00x (tie)
mapcat(reversed) (30 × 1k lists) 95 us 95 us 95 us 1.00x (tie)
concat (30 × 1k lists) 103 us 101 us 101 us 1.01x (tie)
cons (30k ints) 108 us 105 us 104 us 1.02x (tie)
merge (200 dicts × 50 keys) 112 us 109 us 117 us 1.02x (tie)
pluck('a') (10k dicts) 85 us 76 us 107 us 1.12x
sliding_window(3) (30k ints) 1329 us 1133 us 1228 us 1.17x
partition_all(100) (30k ints) 117 us 97 us 102 us 1.21x
interpose (30k ints) 276 us 185 us 717 us 1.49x
last (30k list) 0.07 us 0.03 us 0.06 us 2.65x
frequencies (5 items) 0.31 us 0.09 us 0.42 us 3.41x

How: eager hot functions bypass per-item C-API traffic with a GIL-held Rust-side index (one PyObject_Hash + inline probe per element instead of two dict lookups), CPython's own container fast paths (cached str hashes, compact-int values, identity-first probes), vectorcall for key callbacks, and direct PyList_GET_ITEM iteration over exact-list sources. Lazy functions toolz composes from itertools (take, drop, concat, cons, mapcat) return exactly those C iterators, so they tie by construction. The iterator classes dizzle writes itself sit behind hand-written tp_iternext slot functions instead of PyO3's generated trampoline, and pluck gets a dedicated iterator (one PyObject_GetItem per element) instead of map(itemgetter) — that narrowed interpose from 1.9x to ~1.4–1.5x against Cython's C class and pluck from 1.4x to 1.1x. Inputs exporting a 1-D contiguous integer buffer of any width and signedness (numpy int dtypes, array.array, bytes) take zero-copy native paths in frequencies (eager count, each distinct value boxed once, first-encounter order — keys are Python ints, dict-equal to numpy's own scalars) and keyless unique (lazy native scan): 8–15x faster than cytoolz at 30k elements, 11.9x at 1M np.int64 — and 3.9x faster than np.unique(return_counts=True), since a hash count beats a sort. Keyless topk runs a k-sized min-heap over the same buffers, boxing only the k survivors (17x at 30k, k=10), and isdistinct scans them natively with an early exit (1.5x on fully-distinct data, 100x+ when a duplicate appears early). Floats stay on the iteration path: toolz's dict keeps every NaN scalar as a distinct key, which a native value hash would silently merge. Above ~500k elements frequencies and topk fan out across cores with rayon (the parallel cargo feature, on by default) — frequencies first counts one chunk alone and only fans out when the observed cardinality says the merge will be cheap. 10M np.int64: counted in 12.9ms, 15x faster than np.unique(return_counts=True); topk(10) in 2.4ms, 1.8x faster than np.partition. curry resolves inspect.signature once per chain, making chained partial application ~2x faster than cytoolz. The remaining gaps are floors, not algorithms: the sub-microsecond rows (last, 5-item frequencies) measure fixed call overhead — absolute deltas of 40–220ns per call.

License

MIT.

About

Rust-powered functional toolkit for Python: toolz-compatible API, Option/Result types, and a fluent Iter. benchmarked faster than cytoolz

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages