Grouping Data with Python Tuples
Learn how to group data with Python tuples: tuple dict keys, multi-key grouping, itertools.groupby, namedtuple, Counter, and zip pairing.
Tuples are ideal for grouping data in Python. Because a tuple is immutable and hashable, it can serve as a dictionary key — something a list can never do. This makes tuples the natural choice whenever you need to group records by a combination of fields, track multi-dimensional coordinates, or count composite events.
This chapter covers four practical grouping patterns:
- Tuple as a dictionary key — single-field and multi-field grouping
itertools.groupbywith tuples — streaming grouping over sorted sequencescollections.namedtuple— adding names to grouped recordscollections.Counterwith tuples — counting composite events
Related chapters: Python Tuples · Access Tuples · Loop Tuples · Python Dictionaries · Python Lists Group
Why Tuples Can Be Dictionary Keys
Python requires dictionary keys to be hashable — their value must never change after the key is stored. Tuples satisfy this because they are immutable. Lists do not satisfy it and raise a TypeError when you try to use them as keys.
# A tuple can be a dictionary key
coordinates = {}
coordinates[(10, 20)] = "warehouse A"
coordinates[(30, 40)] = "warehouse B"
print(coordinates[(10, 20)]) # warehouse A
# A list cannot be a dictionary key
try:
d = {[10, 20]: "warehouse A"}
except TypeError as e:
print(f"TypeError: {e}")
# TypeError: unhashable type: 'list'One important gotcha: a tuple that contains a mutable element (such as a list) is also unhashable and cannot be used as a key:
try:
d = {(1, [2, 3]): "value"}
except TypeError as e:
print(f"TypeError: {e}")
# TypeError: unhashable type: 'list'Keep tuple keys composed entirely of immutable values — strings, numbers, booleans, or other tuples.
Grouping by a Single Tuple Field
The simplest use case is unpacking a sequence of tuples and grouping by one element. Use collections.defaultdict(list) to avoid the boilerplate of checking whether a key already exists.
from collections import defaultdict
employees = [
("Alice", "Engineering"),
("Bob", "Marketing"),
("Carol", "Engineering"),
("Dave", "Marketing"),
("Eve", "Engineering"),
]
by_dept = defaultdict(list)
for name, dept in employees:
by_dept[dept].append(name)
for dept, members in sorted(by_dept.items()):
print(f"{dept}: {members}")
# Engineering: ['Alice', 'Carol', 'Eve']
# Marketing: ['Bob', 'Dave']defaultdict(list) automatically creates an empty list the first time a new dept key is seen, so no if dept not in by_dept guard is needed.
Multi-Key Grouping with a Tuple Key
The real power of tuple keys emerges when you need to group by more than one field at a time. Combine the fields into a tuple and use that tuple as the dictionary key.
from collections import defaultdict
records = [
("Alice", "Engineering", "Senior"),
("Bob", "Marketing", "Junior"),
("Carol", "Engineering", "Junior"),
("Dave", "Marketing", "Senior"),
("Eve", "Engineering", "Senior"),
]
# Group by (department, level) — a two-field composite key
grouped = defaultdict(list)
for name, dept, level in records:
grouped[(dept, level)].append(name)
for (dept, level), names in sorted(grouped.items()):
print(f"{dept} / {level}: {names}")
# Engineering / Junior: ['Carol']
# Engineering / Senior: ['Alice', 'Eve']
# Marketing / Junior: ['Bob']
# Marketing / Senior: ['Dave']Because (dept, level) is itself a tuple, it is hashable and can serve as a dict key no matter how many fields it contains. Destructuring the key with for (dept, level), names in ... keeps the code readable.
Grid and coordinate grouping
Multi-key tuple grouping also handles spatial data naturally:
points = [(0, 0), (1, 2), (0, 1), (1, 3), (2, 4)]
from collections import defaultdict
by_x = defaultdict(list)
for x, y in points:
by_x[x].append(y)
for x, ys in sorted(by_x.items()):
print(f"x={x}: y-values={ys}")
# x=0: y-values=[0, 1]
# x=1: y-values=[2, 3]
# x=2: y-values=[4]Grouping with itertools.groupby
itertools.groupby groups consecutive elements that share the same key. It is memory-efficient because it is lazy — it does not load all groups into memory at once. The trade-off is that the input must be sorted by the same key before passing it to groupby, otherwise you get multiple partial groups for the same key instead of one.
from itertools import groupby
sales = [
("East", "Q1", 1200),
("East", "Q2", 1500),
("West", "Q1", 900),
("West", "Q2", 1100),
("East", "Q3", 1800),
]
# Sort by region (index 0) before grouping
sales_sorted = sorted(sales, key=lambda t: t[0])
for region, group in groupby(sales_sorted, key=lambda t: t[0]):
items = list(group)
total = sum(q[2] for q in items)
print(f"{region}: total={total}, quarters={[q[1] for q in items]}")
# East: total=4500, quarters=['Q1', 'Q2', 'Q3']
# West: total=2000, quarters=['Q1', 'Q2']Two things to remember when using groupby with tuples:
- Sort first. Without sorting, each new run of the same key value creates a separate group.
- Consume the group iterator immediately. The inner iterator
groupis exhausted when the outer loop moves to the next key. Always calllist(group)inside the loop body before using it elsewhere.
When groupby is preferable to defaultdict
Use groupby when processing a large, already-sorted sequence where you do not want to load the full grouped result into memory. For general-purpose grouping with no sorting guarantee, defaultdict(list) is simpler and more reliable.
Grouping with collections.namedtuple
namedtuple lets you give names to tuple fields, making grouped data self-documenting. Once you define the namedtuple type, instances behave exactly like regular tuples — they are immutable, hashable, and iterable — but fields are accessible by name as well as by index.
from collections import namedtuple, defaultdict
Employee = namedtuple("Employee", ["name", "department", "salary"])
employees = [
Employee("Alice", "Engineering", 95000),
Employee("Bob", "Marketing", 72000),
Employee("Carol", "Engineering", 88000),
Employee("Dave", "Marketing", 68000),
Employee("Eve", "Engineering", 102000),
]
by_dept = defaultdict(list)
for emp in employees:
by_dept[emp.department].append(emp)
for dept, members in sorted(by_dept.items()):
avg_salary = sum(e.salary for e in members) / len(members)
print(f"{dept}: {[e.name for e in members]}, avg salary={avg_salary:.0f}")
# Engineering: ['Alice', 'Carol', 'Eve'], avg salary=95000
# Marketing: ['Bob', 'Dave'], avg salary=70000Notice that emp.department and emp.salary read more clearly than emp[1] and emp[2]. The namedtuple approach is especially useful when the tuple has many fields and positional indexing becomes hard to follow.
Counting Composite Events with Counter
collections.Counter counts hashable objects. When the "object" you want to count is a combination of values, wrap those values in a tuple and pass the tuple sequence to Counter.
from collections import Counter
log = [
("GET", 200),
("POST", 201),
("GET", 200),
("GET", 404),
("POST", 500),
("GET", 200),
("DELETE", 204),
]
counts = Counter(log)
for entry, n in counts.most_common():
method, status = entry
print(f"{method} {status}: {n} times")
# GET 200: 3 times
# POST 201: 1 times
# GET 404: 1 times
# POST 500: 1 times
# DELETE 204: 1 timesCounter uses the tuple as a hash key internally, so every unique (method, status) combination is tracked separately with no manual grouping code required.
Building Tuple Groups with zip
zip pairs elements from two or more sequences into tuples. This is a natural way to assemble grouped records from parallel lists before applying a grouping operation.
from collections import defaultdict
names = ["Alice", "Bob", "Carol"]
scores = [95, 87, 92]
departments = ["Engineering", "Marketing", "Engineering"]
# Pair the three sequences into tuples
records = list(zip(names, scores, departments))
print(records)
# [('Alice', 95, 'Engineering'), ('Bob', 87, 'Marketing'), ('Carol', 92, 'Engineering')]
# Now group by department
by_dept = defaultdict(list)
for name, score, dept in records:
by_dept[dept].append((name, score))
for dept, members in sorted(by_dept.items()):
print(f"{dept}: {members}")
# Engineering: [('Alice', 95), ('Carol', 92)]
# Marketing: [('Bob', 87)]Choosing the Right Grouping Tool
| Goal | Best tool |
|---|---|
| Group by one field from a list of tuples | defaultdict(list) |
| Group by two or more fields simultaneously | defaultdict(list) with tuple key |
| Stream-group a large pre-sorted sequence | itertools.groupby |
| Add field names to grouped records | collections.namedtuple |
| Count occurrences of composite events | collections.Counter |
| Assemble parallel lists into grouped tuples | zip |
Common Pitfalls
Forgetting to sort before groupby. itertools.groupby only merges consecutive identical keys. If the same key appears in multiple non-consecutive positions, each run becomes a separate group. Always sort by the same key function before calling groupby.
Using a mutable value inside a tuple key. A tuple containing a list is not hashable and raises TypeError when used as a dict key. Keep tuple keys composed of strings, numbers, booleans, or nested tuples.
Consuming the groupby iterator more than once. The group sub-iterator from groupby is exhausted once the outer loop advances. Call list(group) inside the loop body if you need to iterate the group more than once.
Treating defaultdict output as a plain dict. A defaultdict automatically creates new keys when you read a missing key, which can silently populate the dict with empty lists. If you need to check key existence without creating new entries, convert to a plain dict first: dict(grouped).
Related Topics
- Python Tuples — creating, indexing, and slicing tuples
- Access Tuples — indexing, slicing, and membership testing
- Loop Tuples — iterating over a tuple with
forandwhile - Unpack Tuples — assigning tuple elements to variables in one line
- Update Tuples — workarounds for modifying immutable tuples
- Join Tuples — concatenation and the
tuple()constructor - Tuple Methods —
count()andindex()in depth - Python Dictionaries — the key-value store that makes tuple-key grouping possible
- Python Lists Group — grouping lists with
defaultdict,groupby, and dict comprehensions - Python Collections Module —
defaultdict,Counter,namedtuple, and more