Introduction
When building a dictionary where values are lists, you need to handle the case where a key does not yet exist. Appending to a missing key raises KeyError. The cleanest solutions are collections.defaultdict(list), which auto-creates empty lists for new keys, and dict.setdefault(key, []).append(value), which initializes and appends in one call. Both eliminate the manual "check if key exists, create list if not, then append" pattern.
The Problem
data = {}
data["fruits"].append("apple")
# KeyError: 'fruits'
The key "fruits" does not exist, so there is no list to append to.
Method 1: defaultdict(list) - Recommended
1from collections import defaultdict
2
3data = defaultdict(list)
4
5data["fruits"].append("apple")
6data["fruits"].append("banana")
7data["vegetables"].append("carrot")
8data["fruits"].append("cherry")
9
10print(dict(data))
11# {'fruits': ['apple', 'banana', 'cherry'], 'vegetables': ['carrot']}
When you access data["fruits"] and it does not exist, defaultdict calls list() to create an empty list, stores it, and returns it. The .append() then works normally.
Method 2: dict.setdefault()
1data = {}
2
3data.setdefault("fruits", []).append("apple")
4data.setdefault("fruits", []).append("banana")
5data.setdefault("vegetables", []).append("carrot")
6
7print(data)
8# {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']}
setdefault(key, default) returns the existing value if the key exists, or sets and returns the default if it does not. Chaining .append() adds to the returned list.
Method 3: Manual Check
1data = {}
2key = "fruits"
3value = "apple"
4
5if key not in data:
6 data[key] = []
7data[key].append(value)
Explicit but verbose. Use defaultdict or setdefault instead.
Method 4: Try/Except
1data = {}
2
3try:
4 data["fruits"].append("apple")
5except KeyError:
6 data["fruits"] = ["apple"]
This follows Python's "easier to ask forgiveness than permission" (EAFP) principle, but it is clumsier than setdefault for this use case.
Comparison
1from collections import defaultdict
2
3items = [("fruit", "apple"), ("fruit", "banana"), ("veggie", "carrot"),
4 ("fruit", "cherry"), ("veggie", "pea")]
5
6# defaultdict - cleanest
7d1 = defaultdict(list)
8for k, v in items:
9 d1[k].append(v)
10
11# setdefault - no import needed
12d2 = {}
13for k, v in items:
14 d2.setdefault(k, []).append(v)
15
16# Manual check - most explicit
17d3 = {}
18for k, v in items:
19 if k not in d3:
20 d3[k] = []
21 d3[k].append(v)
22
23# All produce: {'fruit': ['apple', 'banana', 'cherry'], 'veggie': ['carrot', 'pea']}
Creating Key or Incrementing a Counter
The same pattern applies to counters:
1from collections import defaultdict
2
3# Counting occurrences
4counter = defaultdict(int)
5words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
6
7for word in words:
8 counter[word] += 1
9
10print(dict(counter))
11# {'apple': 3, 'banana': 2, 'cherry': 1}
12
13# Or use collections.Counter directly
14from collections import Counter
15counter = Counter(words)
Creating Key or Adding to a Set
1from collections import defaultdict
2
3# Unique values per key
4data = defaultdict(set)
5data["fruits"].add("apple")
6data["fruits"].add("banana")
7data["fruits"].add("apple") # Duplicate - ignored
8
9print(dict(data))
10# {'fruits': {'apple', 'banana'}}
Real-World Example: Grouping Records
1from collections import defaultdict
2
3students = [
4 {"name": "Alice", "grade": "A"},
5 {"name": "Bob", "grade": "B"},
6 {"name": "Charlie", "grade": "A"},
7 {"name": "Diana", "grade": "C"},
8 {"name": "Eve", "grade": "B"},
9]
10
11by_grade = defaultdict(list)
12for student in students:
13 by_grade[student["grade"]].append(student["name"])
14
15print(dict(by_grade))
16# {'A': ['Alice', 'Charlie'], 'B': ['Bob', 'Eve'], 'C': ['Diana']}
Nested defaultdict
1from collections import defaultdict
2
3# Two-level grouping
4sales = defaultdict(lambda: defaultdict(list))
5
6sales["2025"]["Q1"].append(1000)
7sales["2025"]["Q1"].append(1200)
8sales["2025"]["Q2"].append(1500)
9sales["2024"]["Q4"].append(900)
10
11print(sales["2025"]["Q1"]) # [1000, 1200]
Other Languages
JavaScript
1const data = {};
2const key = "fruits";
3
4// Using logical OR
5(data[key] = data[key] || []).push("apple");
6
7// Using nullish coalescing
8(data[key] ??= []).push("apple");
Java
1Map<String, List<String>> data = new HashMap<>();
2
3// computeIfAbsent - creates list if key is absent
4data.computeIfAbsent("fruits", k -> new ArrayList<>()).add("apple");
5data.computeIfAbsent("fruits", k -> new ArrayList<>()).add("banana");
Ruby
data = Hash.new { |h, k| h[k] = [] }
data["fruits"] << "apple"
data["fruits"] << "banana"
Common Pitfalls
dict.fromkeys(keys, []) shares one list: All keys point to the same list object. Use {k: [] for k in keys} instead to create independent lists.
**defaultdict(list) vs defaultdict([])**: defaultdicttakes a callable.listis callable (creates[]). []is not callable - raisesTypeError`.
Accessing defaultdict creates keys: if key in d is safe, but d[key] creates the key with a default value even if you only wanted to check. Use key in d for existence checks.
JSON serialization: json.dumps() works with defaultdict, but the default factory is lost. Convert to dict() first if round-tripping matters.
Thread safety: Neither defaultdict nor setdefault is atomic. In multi-threaded code, use threading.Lock or concurrent.futures.
Summary
Use defaultdict(list) for the cleanest auto-creating dictionary of lists
Use dict.setdefault(key, []).append(value) when you want a plain dict with no imports
Never use dict.fromkeys(keys, []) - all keys share the same list object
The pattern works with set, int, or any callable as the default factory
In JavaScript use ??=, in Java use computeIfAbsent, in Ruby use Hash.new with a block