Python Lists Group
Learn three ways to group Python lists: defaultdict, itertools.groupby, and dict comprehensions — with runnable examples and common pitfalls.
Grouping a list means partitioning its elements into sub-collections that share a common key — for example, grouping words by their first letter, or grouping records by a category field. Python offers three main approaches: a manual loop with collections.defaultdict, itertools.groupby from the standard library, and dict comprehensions. This chapter explains each technique, when to choose one over another, and the pitfalls to avoid.
Related chapters: Python Lists · List Methods · List Comprehension · Loop Lists · collections Module
What "grouping" means
Given a flat list and a key function that maps each element to a group label, the goal is to produce a mapping from each label to the list of elements that belong to it:
['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
key = first letter
→ {'a': ['apple', 'avocado', 'apricot'],
'b': ['banana', 'blueberry'],
'c': ['cherry']}The three techniques below all produce this kind of result. They differ in verbosity, performance, and the constraints they place on the input.
Technique 1: Manual loop with defaultdict
collections.defaultdict is the most common and flexible approach. When you access a key that does not yet exist, a defaultdict(list) automatically creates an empty list for that key, so you never need an if key in d guard.
from collections import defaultdict
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
by_letter = defaultdict(list)
for word in words:
by_letter[word[0]].append(word)
for letter, group in sorted(by_letter.items()):
print(f'{letter}: {group}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']Why use defaultdict instead of a plain dict?
With a plain dict you need an explicit check before the first append:
# Plain dict — more boilerplate, same result
by_letter = {}
for word in words:
if word[0] not in by_letter:
by_letter[word[0]] = []
by_letter[word[0]].append(word)A shorter alternative with a plain dict is dict.setdefault:
by_letter = {}
for word in words:
by_letter.setdefault(word[0], []).append(word)setdefault is fine for short scripts, but defaultdict is faster (no repeated key lookups) and more explicit about intent.
Grouping by a computed key
The key can be any expression, not just an attribute. Here, a list of integers is split into even and odd groups:
from collections import defaultdict
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
by_parity = defaultdict(list)
for n in numbers:
by_parity['even' if n % 2 == 0 else 'odd'].append(n)
print('even:', sorted(by_parity['even'])) # even: [2, 4, 6]
print('odd:', sorted(by_parity['odd'])) # odd: [1, 1, 3, 3, 5, 5, 5, 9]Grouping a list of dicts
This is the most common real-world scenario — grouping rows of data by a field value:
from collections import defaultdict
data = [
{'category': 'fruit', 'name': 'apple'},
{'category': 'vegetable', 'name': 'carrot'},
{'category': 'fruit', 'name': 'banana'},
{'category': 'vegetable', 'name': 'broccoli'},
]
grouped = defaultdict(list)
for item in data:
grouped[item['category']].append(item['name'])
for category, names in grouped.items():
print(f'{category}: {names}')
# fruit: ['apple', 'banana']
# vegetable: ['carrot', 'broccoli']Technique 2: itertools.groupby
itertools.groupby groups consecutive elements that share the same key. It is useful when you need to preserve run-length structure or when the data is already sorted and you want to avoid building the whole dictionary at once (it is lazy/streaming).
from itertools import groupby
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
# groupby only groups consecutive elements, so sort first
words_sorted = sorted(words, key=lambda w: w[0])
for letter, group in groupby(words_sorted, key=lambda w: w[0]):
print(f'{letter}: {list(group)}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']The critical gotcha: sort before groupby
groupby only groups consecutive items that share the same key. If the input is not sorted by the key, you get multiple small groups instead of one group per key:
from itertools import groupby
# Unsorted input — groupby produces WRONG results
numbers = [1, 1, 2, 3, 3, 1, 2, 2]
for key, group in groupby(numbers):
print(f'{key}: {list(group)}')
# 1: [1, 1] ← first run of 1s
# 2: [2]
# 3: [3, 3]
# 1: [1] ← second run of 1s — NOT merged with the first!
# 2: [2, 2]Always sort by the same key function before calling groupby:
numbers_sorted = sorted(numbers)
for key, group in groupby(numbers_sorted):
print(f'{key}: {list(group)}')
# 1: [1, 1, 1]
# 2: [2, 2, 2]
# 3: [3, 3]When groupby shines: streaming large data
Because groupby returns an iterator, it does not load all groups into memory at once. This makes it useful for processing large sorted files line by line without building a full dictionary.
from itertools import groupby
# Grouping namedtuple records
from collections import namedtuple
Product = namedtuple('Product', ['category', 'name', 'price'])
products = [
Product('dairy', 'milk', 1.10),
Product('fruit', 'apple', 1.20),
Product('fruit', 'banana', 0.50),
Product('vegetable', 'broccoli', 1.50),
Product('vegetable', 'carrot', 0.80),
]
# products is already sorted by category here
for category, group in groupby(products, key=lambda p: p.category):
items = list(group)
print(f'{category}: {[p.name for p in items]}')
# dairy: ['milk']
# fruit: ['apple', 'banana']
# vegetable: ['broccoli', 'carrot']Technique 3: Dict comprehension
A dict comprehension builds the grouped dict in one expression. It is concise but has a downside: the inner list comprehension re-scans the entire input for every unique key, making it O(n × k) where k is the number of unique keys. For small lists this is fine; for large lists, prefer defaultdict.
words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry', 'apricot']
# Collect unique keys first, then build each group
letters = sorted(set(w[0] for w in words))
grouped = {letter: [w for w in words if w[0] == letter] for letter in letters}
for letter, group in grouped.items():
print(f'{letter}: {group}')
# a: ['apple', 'avocado', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']This technique is most readable when the key set is small and already known — for example, grouping True/False results or a fixed set of categories.
Aggregating groups after grouping
A common follow-up to grouping is aggregation: computing a sum, average, minimum, or count per group. Combine defaultdict(list) with standard Python arithmetic:
from collections import defaultdict
scores = [
('Alice', 90), ('Bob', 75), ('Alice', 85),
('Bob', 88), ('Carol', 92),
]
by_student = defaultdict(list)
for name, score in scores:
by_student[name].append(score)
for student, student_scores in sorted(by_student.items()):
avg = sum(student_scores) / len(student_scores)
print(f'{student}: scores={student_scores}, avg={avg:.1f}')
# Alice: scores=[90, 85], avg=87.5
# Bob: scores=[75, 88], avg=81.5
# Carol: scores=[92], avg=92.0Choosing the right technique
| Situation | Best choice |
|---|---|
| General grouping, any order | defaultdict(list) |
| Need to stream large sorted data | itertools.groupby |
| Small list, concise one-liner | Dict comprehension |
| Input is already sorted | Either defaultdict or groupby |
| Need to aggregate (sum, avg, etc.) | defaultdict(list) + arithmetic |
Common pitfalls
Forgetting to sort before groupby. groupby only merges consecutive identical keys. Always sorted() the input by the same key function before passing it to groupby.
Assigning list(group) immediately. The group iterator from groupby is exhausted as soon as the outer for loop advances to the next key. Convert it to a list inside the loop body if you need to use it more than once.
Modifying the input list while grouping. Adding or removing elements from the list during a grouping loop produces unpredictable results. Build the grouped dict first, then modify elements.
defaultdict appearing in repr. defaultdict(list, {...}) looks different from a plain dict in repr. Wrap it with dict(grouped) when you need a plain dict output.