W3docs

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.groupby with tuples — streaming grouping over sorted sequences
  • collections.namedtuple — adding names to grouped records
  • collections.Counter with 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:

  1. Sort first. Without sorting, each new run of the same key value creates a separate group.
  2. Consume the group iterator immediately. The inner iterator group is exhausted when the outer loop moves to the next key. Always call list(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=70000

Notice 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 times

Counter 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

GoalBest tool
Group by one field from a list of tuplesdefaultdict(list)
Group by two or more fields simultaneouslydefaultdict(list) with tuple key
Stream-group a large pre-sorted sequenceitertools.groupby
Add field names to grouped recordscollections.namedtuple
Count occurrences of composite eventscollections.Counter
Assemble parallel lists into grouped tupleszip

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).

Practice

Practice
Which of the following can be used as a Python dictionary key?
Which of the following can be used as a Python dictionary key?
Was this page helpful?