toolz review
Toolz 1.1.0 is a pure-Python collection for iterator processing, mapping updates, currying, and function composition. `itertoolz` includes grouping, batching, sliding windows, frequencies, and reductions by key. `dicttoolz` creates updated mappings, while `functoolz` provides `curry`, `pipe`, `compose`, `memoize`, and related call helpers. `toolz.curried` rearranges calls for data-last pipelines, and `tlz` can select cytoolz when available. Version 1.1.0 adds Python 3.14 and PyPy 3.11 support, drops Python 3.8, fixes a `partition_all` edge case, and speeds `merge_sorted`.
Toolz 1.1.0 installed in 0.3 seconds as 1 MB with 0 dependencies, and `tlz` imported in 0.14 seconds with 0 pip-audit findings in our sandbox. Keep it where its iterator and mapping vocabulary already pays rent; check the standard library first in new code, and do not expect active feature development or packaged typing.
We installed it
| Install | ✓ · 0.3s | 1 package on disk · 1 MB |
| Import | ✓ | import tlz in 0.14s · pure Python · requires Python >=3.9 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does toolz install cleanly?
Yes. In a fresh container with an empty cache, pip install toolz finished in 0.3s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does toolz need to run?
Python >=3.9, and nothing compiled: it is pure Python. In our run import tlz succeeded in 0.14s.
toolz or cytoolz: which should you use?
cytoolz: Use it after profiling shows Toolz-compatible transformations are CPU-bound and a compiled wheel is acceptable. Toolz 1.1.0 installed in 0.3 seconds as 1 MB with 0 dependencies, and tlz imported in 0.14 seconds with 0 pip-audit findings in our sandbox.
When should you not use toolz?
You expect new features or quick review of enhancement requests. The README calls Toolz alive but inactive and limits commitment to critical fixes, security, and Python-version updates.
Use it if
- Existing Python code already expresses grouping, batching, nested mapping updates, or per-key reductions with Toolz vocabulary.
- A data pipeline benefits from lazy one-pass transformations and curried functions whose collection arrives last.
- Mapping updates should return new outer structures instead of mutating the original dictionary path.
- The same imports should use compiled cytoolz when installed and pure Python Toolz as a fallback through `tlz`.
- You expect new features or quick review of enhancement requests. The README calls Toolz alive but inactive and limits commitment to critical fixes, security, and Python-version updates.
- Installed packages must expose a typed interface. Toolz 1.1.0 does not ship `py.typed`, so a type checker cannot treat its implementation annotations as a supported package contract.
- The needed operation is already clear in `itertools`, `functools`, `collections`, or modern dictionary syntax. One call to `batched`, `pairwise`, `Counter`, `cache`, or mapping union does not justify another vocabulary.
- Profiling shows these helpers on a CPU hot path. Toolz adds Python function calls; cytoolz is the compatible compiled option when its wheel supports the target.
- Reviewers struggle with curried data-last code. `toolz.curried` shadows names such as `map` and `filter`, and a partial call can return another function instead of revealing a missing argument immediately.
Setup reality
We installed Toolz 1.1.0 in 0.3 seconds in a fresh Python 3.12 container. It left 1 package using 1 MB and has 0 direct dependencies. The distribution is pure Python, requires Python 3.9 or newer, and does not include py.typed. Our measured package metadata did not identify a license, although the project README points to its New BSD license file. pip-audit found 0 known vulnerabilities. import tlz worked in 0.14 seconds.
Import style changes how code reads. toolz exposes ordinary functions, toolz.curried returns functions that can accept arguments across several calls, and tlz chooses cytoolz when present or Toolz otherwise. Curried imports can hide Python built-ins, so a module namespace is easier to audit in mixed code. Supplying too few arguments to a curried function can produce another callable and move the eventual error away from the original call site.
Many iterator helpers are lazy and single-use. take, map, filter, concat, partition, and sliding_window do not return reusable lists. groupby is different because it consumes the source and stores lists in a dictionary. partition drops an incomplete tail unless padding is supplied, while partition_all keeps it. Version 1.1.0 now raises IndexError for an object whose reported length exceeds the items it yields.
Argument order needs checking rather than guessing. groupby(key, seq), valmap(func, mapping), and get_in(keys, mapping) put data last. assoc(mapping, key, value) and update_in(mapping, keys, func) start with the mapping. memoize can grow without a supplied cache policy. In reduceby, a shared mutable initial accumulator can leak state between groups, so prefer an immutable starter or a callable initializer supported by the chosen use.
Patterns
Group records by a field group-records
from toolz import groupby
rows = [
{'id': 1, 'state': 'open'},
{'id': 2, 'state': 'closed'},
{'id': 3, 'state': 'open'},
]
by_state = groupby(lambda row: row['state'], rows)Toolz consumes the source into a dictionary of lists; unlike `itertools.groupby`, the input need not be sorted.
Count values and derived keys count-groups
from toolz import countby, frequencies
status_counts = countby(lambda row: row['state'], rows)
letter_counts = frequencies('mississippi')`frequencies` counts items directly, while `countby` computes one key for each item first.
Compose a lazy curried pipeline build-pipeline
from toolz import pipe
from toolz.curried import filter, map, take
result = pipe(
range(10_000),
filter(lambda n: n % 7 == 0),
map(lambda n: n * n),
take(5),
list,
)Curried `map` and `filter` shadow built-ins; evaluation begins at `list`, and `take(5)` limits consumption.
Read a nested path with a default read-nested-value
from toolz import get_in
document = {'user': {'profile': {'visits': 3}}}
visits = get_in(['user', 'profile', 'visits'], document)
email = get_in(['user', 'email'], document, default=None)`get_in` puts the path before the mapping; a missing path uses the supplied default.
Create a nested mapping update update-nested-value
from toolz import assoc_in, update_in
with_tier = assoc_in(document, ['user', 'profile', 'tier'], 'pro')
with_visit = update_in(
document, ['user', 'profile', 'visits'], lambda n: n + 1
)The affected mapping path is copied and the original outer mapping is left unchanged.
Resolve duplicate keys with a function merge-mappings
from toolz import merge_with
totals = merge_with(
sum,
{'paper': 4, 'ink': 1},
{'paper': 3, 'toner': 2},
)The collision function receives a list of values; plain last-wins merging already exists in dictionary union.
Map and filter dictionary values transform-mapping
from toolz import valfilter, valmap
prices = {'paper': 450, 'ink': 1200, 'toner': 6800}
dollars = valmap(lambda cents: cents / 100, prices)
expensive = valfilter(lambda cents: cents >= 1000, prices)Both return new mappings; use `itemmap` when the transformation needs each key-value pair.
Keep the last short batch batch-iterator
from toolz import partition_all
for batch in partition_all(500, source_rows):
write_batch(batch)`partition_all` yields tuples and preserves the incomplete tail; `partition` drops it unless padding is provided.
Calculate changes between neighbours slide-window
from toolz import sliding_window
changes = (
current - previous
for previous, current in sliding_window(2, values)
)The result is one-pass, and a window larger than the input yields no items.
Aggregate without storing group lists reduce-by-key
from toolz import reduceby
totals = reduceby(
lambda order: order['customer'],
lambda total, order: total + order['amount'],
orders,
0,
)`reduceby` retains one accumulator per key; avoid a mutable initializer shared across groups.
Run functions from left to right compose-functions
from toolz import compose_left
slugify = compose_left(
str.strip,
str.lower,
lambda value: value.replace(' ', '-'),
)
slug = slugify(' Open Source Guide ')`compose_left` follows reading order, while `compose` starts with the rightmost function.
Import through the backend selector select-backend
from tlz import pipe
from tlz.curried import groupby, map
result = pipe(records, map(normalize), groupby(owner_key))`tlz` uses cytoolz when installed and otherwise falls back to Toolz; test both if that optional swap is supported.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| cytoolz | PyPI | Use it after profiling shows Toolz-compatible transformations are CPU-bound and a compiled wheel is acceptable. |
| more-itertools | PyPI | Use it for a wider set of iterator recipes without adopting Toolz's mapping and currying style. |
| funcy | PyPI | Use it when Funcy's sequence, collection, and decorator choices fit the team's functional style better. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

