The same grouping three ways. The first one is what everybody writes; the other two are what it means.
# The dance
groups = {}
for item in items:
if item['key'] not in groups:
groups[item['key']] = []
groups[item['key']].append(item)
# setdefault: fetch the list, creating it if needed, and return it
groups.setdefault(item['key'], []).append(item)
# defaultdict: the dictionary itself makes the missing value
from collections import defaultdict
groups = defaultdict(list)
groups[item['key']].append(item)
- setdefault returns the value, which is why it chains
It looks up the key, inserts the default when it is missing, and returns whatever is now there — so .append() on the end operates on the real list inside the dictionary. The name is unhelpful; read it as 'get_or_create'. One caveat: the default is built on every call, even when the key already exists, so an expensive default is a reason to prefer defaultdict.
- defaultdict moves the decision into the dictionary
Constructed with a factory, it calls it whenever a missing key is read, so the loop body becomes a plain append. It is the cleanest version — and the factory fires on *any* missing key, including one a caller merely asked about, which silently creates entries. That is why a defaultdict is a fine local and a poor return value.
- Both preserve insertion order, because both are dicts
Neither changes the ordering guarantee — groups appear in the order their first member arrived, and the lists keep their own order because append does. Nothing about removing the branch touches either, which is what makes this a safe refactor rather than a rewrite.
The three lines in the loop are one operation: get the list for this status, making it if it does not exist, and append to it. Python has a method that is exactly that.
groups.setdefault(status, []).append(ticket) replaces the if, the assignment and the append. Or build a defaultdict(list) and append straight into it.
If you take the defaultdict route, remember to return dict(groups) — the contract says a plain dict, and handing back a defaultdict gives every caller a dictionary that quietly grows when they read from it.
def by_status(tickets): groups = {} for ticket in tickets: groups.____(ticket['status'], ____).append(ticket) return groups
def by_status(tickets):
"""Group tickets by their status."""
groups = {}
for ticket in tickets:
# get-or-create, then append to whatever is now there.
groups.setdefault(ticket["status"], []).append(ticket)
return groups
Both remove the branch. defaultdict needs an extra dict() on the way out to satisfy the contract, and forgetting it is the bug it is famous for — a caller who asks whether 'closed' is in the result creates it. setdefault keeps the dictionary ordinary throughout, which is the right default when the dictionary leaves the function.
setdefault evaluates its default before it knows whether the key exists, so a throwaway [] is created on every loop. For an empty list that is nothing. For an expensive default — a database connection, a compiled pattern — it is the reason defaultdict exists, since its factory only runs on an actual miss.
The lists hold the caller’s ticket dictionaries, so a caller who updates one sees the change through both the group and the original list. That is almost always what is wanted — copying every record to group them would be expensive and surprising — but it is worth knowing before you annotate the grouped results in place.
For a straightforward grouping over a whole list, this is the answer. When the input is already sorted by the key, itertools.groupby is lazier and expresses the consecutive-runs meaning directly — and gets a completely different answer on unsorted input, which catches people out. If you only need counts rather than the items, collections.Counter is one line and says so. And if the same grouping appears in five places, write it once as a group_by(records, key) helper rather than the loop — that is the exercise this refactor leads into.
- Write the defaultdict version and confirm the plain-dict case fails until you wrap it.
- Take a key function instead of hard-coding 'status', and see how little of the loop changes.
- Group by two fields at once with a tuple key, and work out what that does to the first-seen ordering.