Metadata-Version: 2.4
Name: dizzle
Version: 0.7.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
License-File: LICENSE
Summary: Rust-powered functional toolkit: toolz-compatible functions, Option/Result, fluent Iter.
Author-email: 4thel00z <4thel00z@gmail.com>
License-Expression: MIT
Requires-Python: >=3.13
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/4thel00z/dizzle

# dizzle

![dizzle.png](https://raw.githubusercontent.com/4thel00z/logos/master/dizzle.png)

Rust-powered functional toolkit for Python 3.13+: the [toolz](https://github.com/pytoolz/toolz)
API you know, an `Option`/`Result` you've wanted, and a fluent `Iter` — all
executing in native code.

## Install

```console
pip install dizzle  # or: uv add dizzle
```

## The three layers

```python
# 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

```console
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:

```console
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.12x** (8–15x faster), putting the full 18-group suite at **0.82x**:

| benchmark | dizzle | cytoolz | toolz | vs cytoolz |
|---|---|---|---|---|
| `frequencies` (30k int64 buffer) | 51 us | 740 us | 1138 us | **0.07x** |
| `unique` (30k int64 buffer) | 45 us | 373 us | 421 us | **0.12x** |
| `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. `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.

