Group with setdefault or defaultdict(list) — one pass, one lookup per record, no scanning for an existing group. Inverting is a one-line comprehension, with two traps: duplicate values silently collapse, and unhashable values raise.
Overview
The two operations
Grouping turns a flat sequence into a dictionary of lists, keyed on some property. Inverting swaps keys and values. Both are one-liners once the right tool is known, and both have a trap.
1Python
words = ["apple", "avocado", "banana", "blueberry", "cherry"]
# Grouping by first letter
from collections import defaultdict
groups = defaultdict(list)
for w in words:
groups[w[0]].append(w)
# {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}
print("grouped:", dict(groups))
Output
defaultdict(list) is what makes this three lines instead of five. Without it, every append needs a guard:
2Python
from collections import Counter, defaultdict
from itertools import groupby
words = ["apple", "avocado", "banana", "blueberry", "cherry"]
ages = {"alice": 30, "bob": 25, "carol": 30}
sales = [("alice", 10), ("bob", 5), ("alice", 7)]
groups = {}
for w in words:
if w[0] not in groups: # the boilerplate defaultdict removes
groups[w[0]] = []
groups[w[0]].append(w)
print("grouped:", groups)
Output
setdefault is the middle ground and works on a plain dict — groups.setdefault(w[0], []).append(w) — which is useful when the result must be a real dict rather than a defaultdict.
Dicts, sets & hashingCoding problemEasy
Step through it
What to watch
Each record costs one lookup and one append.
Groups appear as they are first encountered.
Inversion assumes the values are unique — watch what happens when they are not.
Say this out loud
"defaultdict(list) and append - one pass, O(n). For inverting, a dict comprehension, but I'd check the values are unique first, because duplicates silently overwrite and you lose entries without an error."
Group records and invert a dictionary
Group a list of records by a field. Then invert a dictionary so the values become keys.
Run it
The three grouping idioms agreeing, groupby getting it wrong on unsorted input, and both inversion failures caught in the act.
3Python
# Grouping and inverting: one pass each, and two traps in the inversion.
from collections import defaultdict
from itertools import groupby
people = [("ana", "eng"), ("bo", "sales"), ("cy", "eng"),
("di", "sales"), ("ed", "eng")]
by_setdefault = {}
for name, team in people:
by_setdefault.setdefault(team, []).append(name)
by_default = defaultdict(list)
for name, team in people:
by_default[team].append(name)
print("setdefault :", dict(by_setdefault))
print("defaultdict:", dict(by_default))
print("agree :", by_setdefault == dict(by_default))
# --- groupby groups CONSECUTIVE keys only ------------------------------
unsorted_groups = {k: [n for n, _ in g] for k, g in
groupby(people, key=lambda p: p[1])}
sorted_groups = {k: [n for n, _ in g] for k, g in
groupby(sorted(people, key=lambda p: p[1]), key=lambda p: p[1])}
print()
print("groupby, unsorted:", unsorted_groups, " <- 'eng' appears twice, last wins")
print("groupby, sorted :", sorted_groups)
print("It is not SQL's GROUP BY. Sort by the same key first, or use a dict.")
# --- inverting ---------------------------------------------------------
capital = {"france": "paris", "japan": "tokyo", "italy": "rome"}
print()
print("inverted:", {v: k for k, v in capital.items()})
# Trap 1: duplicate values collapse, silently.
sizes = {"ana": "medium", "bo": "large", "cy": "medium"}
naive = {v: k for k, v in sizes.items()}
grouped = defaultdict(list)
for k, v in sizes.items():
grouped[v].append(k)
print()
print("input :", sizes)
print("naive inversion:", naive, " <- 'ana' vanished")
print("inverted safely:", dict(grouped))
# Trap 2: values that cannot be keys.
scores = {"ana": [90, 80], "bo": [70]}
try:
{v: k for k, v in scores.items()}
except TypeError as e:
print()
print("inverting list values ->", e)
print("A tuple would work:", {tuple(v): k for k, v in scores.items()})
Output
The defaultdict trap
A defaultdictcreates entries on read. Merely looking a key up inserts it:
4Python
from collections import Counter, defaultdict
from itertools import groupby
words = ["apple", "avocado", "banana", "blueberry", "cherry"]
ages = {"alice": 30, "bob": 25, "carol": 30}
sales = [("alice", 10), ("bob", 5), ("alice", 7)]
d = defaultdict(list)
if d["missing"]: # this INSERTS 'missing': []
...
print(len(d)) # 1, not 0
print("missing" in d) # True
print("the lookup inserted a key, so len is", len(d))
Output
That surprises people debugging a size or an iteration. Two ways to avoid it: use d.get(key, []) for reads, or convert to a plain dict once building is finished with dict(d).
The related habit worth having: never pass a defaultdict out of a function as a return value if callers will read arbitrary keys. Convert it first, and the mutation-on-read behaviour stays inside the code that wanted it.
Inverting a dictionary
5Python
ages = {"alice": 30, "bob": 25, "carol": 30}
inverted = {v: k for k, v in ages.items()}
print("inverted:", inverted)
print("alice is gone - carol overwrote her, because both are 30")
Output
Duplicate values collide, and the last one wins. alice and carol are both 30, so only carol survives. This is the single most common bug with inversion, and the fix is to invert into lists:
6Python
from collections import Counter, defaultdict
from itertools import groupby
words = ["apple", "avocado", "banana", "blueberry", "cherry"]
ages = {"alice": 30, "bob": 25, "carol": 30}
sales = [("alice", 10), ("bob", 5), ("alice", 7)]
from collections import defaultdict
inverted = defaultdict(list)
for k, v in ages.items():
inverted[v].append(k)
# {30: ['alice', 'carol'], 25: ['bob']}
print("inverted:", dict(inverted))
Output
Which is the same operation as grouping — grouping the keys by their value. The two topics are one topic.
The other requirement: values must be hashable to become keys. Inverting {"a": [1, 2]} raises TypeError, and converting the list to a tuple first fixes it.
Three ways to group
setdefault(key, []).append(x) — no import, and it makes the default explicit. It does construct an empty list on every call, which is why the next form is usually preferred.
defaultdict(list) — the idiomatic answer. Remember that reading a missing key creates it, so it is not safe to inspect casually.
itertools.groupby — the one that catches people. It groups consecutive equal keys only, so it needs the input sorted by the same key first. It is not the SQL GROUP BY its name suggests.
Two ways inversion breaks
Duplicate values.{v: k for k, v in d.items()} keeps only the last key for each repeated value, silently. If duplicates are possible, invert to lists instead — which is just grouping again, by value.
Unhashable values. A value that was fine as a value cannot necessarily be a key: a dict mapping names to lists of scores cannot be inverted directly, because a list is unhashable.
Where this shows up
Grouping is the shape behind most "organise this data" questions: anagram groups keyed on sorted letters, files keyed on their hash, log lines keyed on their level. Recognising it saves you from writing a nested loop that scans for an existing group — the accidental O(n²) this idiom exists to avoid. The one-pass defaultdict build is O(n); the nested-loop version that re-scans the groups already built is O(n²), which on a hundred thousand records is the difference between ten thousand operations and ten billion — the same idiom, and the reason it is worth reaching for on sight.
itertools.groupby and why it surprises people
The standard library has a groupby, and it does not do what the name suggests to anyone coming from SQL or pandas.
7Python
from itertools import groupby
words = ["apple", "banana", "avocado", "blueberry"]
for key, group in groupby(words, key=lambda w: w[0]):
print(key, list(group))
# a ['apple']
# b ['banana']
# a ['avocado'] <- 'a' appears TWICE
# b ['blueberry']
Output
itertools.groupby groups consecutive runs only. It does not collect scattered items. To get SQL-like behaviour the input must be sorted by the same key first:
8Python
from collections import Counter, defaultdict
from itertools import groupby
words = ["apple", "avocado", "banana", "blueberry", "cherry"]
ages = {"alice": 30, "bob": 25, "carol": 30}
sales = [("alice", 10), ("bob", 5), ("alice", 7)]
for key, group in groupby(sorted(words, key=lambda w: w[0]), key=lambda w: w[0]):
...
for key, group in groupby(sorted(words, key=lambda w: w[0]), key=lambda w: w[0]):
print(key, "->", list(group))
Output
Which costs O(n log n), while the defaultdict version is O(n). Prefer defaultdict unless the data is already sorted or the runs themselves are what you want — compressing repeated values, detecting consecutive duplicates, run-length encoding.
The other sharp edge: the group is an iterator sharing the underlying stream. It is exhausted as soon as you advance to the next group, so list(group) must happen before moving on. groups = [(k, g) for k, g in groupby(x)] yields empty groups for exactly this reason.
The grouping patterns worth having ready
Group by a computed key.
9Python
from collections import Counter, defaultdict
from itertools import groupby
words = ["apple", "avocado", "banana", "blueberry", "cherry"]
ages = {"alice": 30, "bob": 25, "carol": 30}
sales = [("alice", 10), ("bob", 5), ("alice", 7)]
by_length = defaultdict(list)
for w in words:
by_length[len(w)].append(w)
print("by length:", dict(by_length))
Output
Group by several keys — use a tuple:
10Python
from collections import Counter, defaultdict
from itertools import groupby
words = ["apple", "avocado", "banana", "blueberry", "cherry"]
ages = {"alice": 30, "bob": 25, "carol": 30}
sales = [("alice", 10), ("bob", 5), ("alice", 7)]
by_both = defaultdict(list)
for w in words:
by_both[(w[0], len(w))].append(w)
print("by letter and length:", dict(by_both))
Output
Count instead of collect — Counter is the specialised form:
11Python
from collections import Counter, defaultdict
from itertools import groupby
words = ["apple", "avocado", "banana", "blueberry", "cherry"]
ages = {"alice": 30, "bob": 25, "carol": 30}
sales = [("alice", 10), ("bob", 5), ("alice", 7)]
from collections import Counter
print(Counter(w[0] for w in words)) # {'a': 2, 'b': 2, 'c': 1}
Output
Aggregate while grouping — defaultdict(int) for sums, defaultdict(set) for deduplicated members:
12Python
from collections import Counter, defaultdict
from itertools import groupby
words = ["apple", "avocado", "banana", "blueberry", "cherry"]
ages = {"alice": 30, "bob": 25, "carol": 30}
sales = [("alice", 10), ("bob", 5), ("alice", 7)]
totals = defaultdict(int)
for name, amount in sales:
totals[name] += amount
print("totals:", dict(totals))
Output
Group anagrams, the classic interview form — the key is a canonical form:
groups = defaultdict(list)
for w in words:
groups["".join(sorted(w))].append(w)
return list(groups.values())
Need
Tool
Collect items
defaultdict(list)
Count items
Counter
Sum values
defaultdict(int)
Deduplicated members
defaultdict(set)
Nested grouping
defaultdict(lambda: defaultdict(list))
Consecutive runs only
itertools.groupby
The nested case needs a lambda because defaultdict(defaultdict(list)) is wrong — the argument must be a callable that produces the default, not an instance.
Questions people ask
Why defaultdict over setdefault? Cleaner in a loop and slightly faster, since setdefault builds the default list on every call even when the key exists. Use setdefault when the result must be a plain dict.
What happens when inverting with duplicate values? Later keys overwrite earlier ones. Invert into lists to keep all of them.
Can any value become a key? Only hashable ones. Convert lists to tuples.
Why does itertools.groupby split my groups? It only groups consecutive items; sort by the key first.
How do I stop defaultdict inserting on read? Use .get(), or wrap in dict() when done building.
Is Counter a dict? Yes, a subclass — and it returns 0 for missing keys rather than raising, without inserting them.
Recap in one screen
defaultdict(list) is the grouping idiom; setdefault is the plain-dict equivalent.
A defaultdict inserts keys on read — use .get() or convert with dict() before handing it out.
Inverting with a comprehension silently drops duplicate values; invert into lists instead.
Values becoming keys must be hashable.
itertools.groupby groups only consecutive runs and its groups are single-use iterators; sort first, or use defaultdict.
How the code works
The three grouping idioms agreeing, groupby getting it wrong on unsorted input, and both inversion failures caught in the act.
How the code works
setdefault(team, []).append(name)One lookup and one append per record. The alternative — scanning existing groups for a match — is the O(n²) this idiom exists to avoid.
groupby(people, key=...)Groups only consecutive equal keys, so unsorted input produces a group per run and later runs overwrite earlier ones. Sorting first is mandatory.
{v: k for k, v in sizes.items()}Silently loses ana, because two names share a size and the later key wins. No error, one entry gone.
{v: k for k, v in scores.items()}A TypeError: the values are lists, and a key must be hashable. Converting to tuples is the usual fix.
Change one thing
Group by two fields at once using a tuple key. It works because tuples are hashable — the same reason they can be dict keys at all.
Read a missing key from the defaultdict and print it again. The read created an entry, which is the trap that idiom carries.
Where this runs
Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.
Check yourself
0 of 3
Answer without scrolling back up.
itertools.groupby differs from SQL's GROUP BY in that it:
On unsorted input you get one group per run, and later runs overwrite earlier ones when collected into a dict.
Inverting a dict with a comprehension when two keys share a value:
No error is raised and an entry disappears. If duplicates are possible, invert to lists - which is grouping by value.
A dict mapping names to lists of scores cannot be inverted directly because:
Converting each value to a tuple makes it hashable and the inversion legal.
Cheat sheet
Group records and invert a dictionary
Group with setdefault or defaultdict(list) — one pass, one lookup per record, no scanning for an existing group. Inverting is a one-line comprehension, with two traps: duplicate values silently collapse, and unhashable values raise.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.