Hands-on itertools.groupby vs manual dict-of-lists: real records/s grouping pre-sorted key runs, measured on Linux localhost today in this lab for SREs.
4 min read
Group a pre-sorted list of (key, value) records: itertools.groupby vs a manual run-length scan vs defaultdict(list). This lab reports records/s on Linux localhost, and shows what happens if you feed unsorted data to groupby.
It is not Counter tallying (lab 64) or chain/flatten (lab 67), and not the itertools micro-loop bake-off (lab 55).
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. No Docker. Input sorted unless noted.
Verdict up front (50 000 records / 200 keys): groupby consume ~24.21 Mrec/s; manual sorted runs ~18.09; defaultdict ~19.08; groupby→dict lists ~17.24. On shuffled input, naive d[k]=list(grp) lost 49800 / 50000 values.
| Arm | Pattern |
|---|---|
groupby consume | iterate groups, discard values |
groupby → dict of lists | materialize each group |
| manual sorted runs | contiguous key scan |
defaultdict(list) | works unsorted |
dict.setdefault | same idea, slower API |
Script: lab-evidence/98-groupby-vs-manual/results/run_lab.py.
| Arm | Mrec/s |
|---|---|
| groupby consume | 24.21 |
| manual sorted runs | 18.09 |
| defaultdict(list) | 19.08 |
| groupby → dict lists | 17.24 |
| setdefault lists | 15.19 |
At this scale, consume is fastest because it never allocates value lists. Once you materialize, manual runs, groupby→dict, and defaultdict sit in a tight band — list appends dominate.
| Config | groupby→dict | defaultdict | groupby consume |
|---|---|---|---|
| 10k / 100 keys | 27.41 | 25.05 | 43.23 |
| 50k / 200 | 17.24 | 19.08 | 24.21 |
| 200k / 500 | 6.87 | 7.91 | 8.71 |
Throughput falls as group and list sizes grow — expected allocator pressure, not a mystery.
Naive assign-per-run on shuffled data kept only 200 values (lost 49800). Merging runs or using defaultdict kept all 50000. groupby requires sorted (or already-grouped) input.
If your pipeline is unsorted, add sorted(..., key=) cost before celebrating groupby. For one-shot grouping of random keys, defaultdict often wins end-to-end even when per-record rates look close on a pre-sorted fixture. Manual run-length scans assume contiguity the same way groupby does.
groupby when data is already sorted and you stream group iterators.defaultdict(list) wins when order is arbitrary or you do not want a sort tax.setdefault trails defaultdict on this box for the same list-building work.Evidence: summary.json, summary.txt.
One Linux box. Synthetic int values. Sort cost not included in sorted-arm timings (fixture pre-sorted). Warmup discarded; p50 of seven rounds.
On sorted 50 k records, groupby consume ~24.21 Mrec/s led; building dicts sat ~17.24–19.08 Mrec/s. Sort first or use defaultdict — unsorted naive groupby dropped 49800 values here.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5. 50k/200keys: groupby_consume 24.21 Mrec/s; manual_runs 18.09; defaultdict 19.08; groupby_dict 17.24. Unsorted naive lost 49800/50000. Affiliates: 0. Evidence: lab-evidence/98-groupby-vs-manual/.
Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.
Lab1 Oct 20264 min read
Hands-on heapq.merge vs sorted(chain) multi-way merge: real records/s on pre-sorted lists, measured on Linux localhost today in this hands-on lab for SREs.
Lab1 Oct 20264 min read
Hands-on mmap MAP_SHARED write+msync vs pwrite region update: real MB/s with durability labels, measured on Linux localhost in this hands-on lab for SREs.
Lab1 Oct 20264 min read