Dictionary Patterns
Counting, grouping, lookups, reverse maps, caches - the handful of dictionary patterns behind most data problems.
What you will be able to do
- Count anything with count[key] = count.get(key, 0) + 1
- Find duplicates from a frequency dictionary, and the first duplicate in one pass
- Replace long if/elif chains and repeated searches with lookup tables
- Group values under a key: a dictionary of lists
- Build dictionaries from two lists with zip(), and from a list of records by id
- Find the key with the highest or lowest value, and sort a dictionary by value
- Build a reverse lookup, and know what happens when values repeat
- Use a dictionary to validate data, track relationships, and cache results
- Recognise which pattern a new problem needs
The idea, in plain English
Lesson 19 was about what a dictionary is. This lesson is about what it is for. A handful of patterns - counting, looking up, grouping, remembering - cover a surprising share of everyday programming problems, and once you can see them you stop writing nested loops to search lists.
Every pattern has the same shape: input -> dictionary -> result. You walk the input once, and at each item you either read the dictionary ("have I seen this?", "what is this mapped to?") or write to it ("one more of these", "add this to its group"). Because a dictionary looks up a key in roughly constant time, one pass is usually enough.
The core moves are small: count[key] = count.get(key, 0) + 1 to count, lookup[key] to find, groups[key].append(value) to group, if key in seen to detect repeats. The rest of the lesson is those four moves applied to real data.
Every output below comes from running the code in Python 3.11.
Worked example: Counting, grouping and looking up students, words and users.
Pattern 1 - frequency counting
count = {}, then for each item count[item] = count.get(item, 0) + 1. The first time an item appears, get() returns the default 0 and the count becomes 1; after that get() returns the current count and it goes up by one. For [1, 2, 2, 3, 3, 3] the result is {1: 1, 2: 2, 3: 3}.
It counts anything hashable: numbers, characters in a string ("banana" gives {‘b’: 1, ‘a’: 3, ‘n’: 2}), words from sentence.split(), status codes from a log. Do not write count[item] += 1 on its own - on a new item that raises KeyError, because there is no value to add one to yet.
item 1count.get(1, 0) is 0, so count[1] = 1 -> {1: 1}item 2count.get(2, 0) is 0, so count[2] = 1 -> {1: 1, 2: 1}item 2count.get(2, 0) is 1, so count[2] = 2 -> {1: 1, 2: 2}Tip: collections.Counter does this in one line - Counter(words).most_common(2) - and you will meet it in the standard-library module. Learn the get() version first: it is what Counter is doing for you.
Pattern 2 - duplicates
Two questions, two patterns. "Which values repeat?" - count first, then keep the keys whose count is above 1: [1, 2, 3, 2, 4, 3, 5] gives [2, 3]. "Which value repeats first?" - you do not need full counts; walk the list, remember what you have seen, and stop at the first item already remembered.
In [4, 2, 7, 2, 9, 4] the first duplicate is 2, not 4: 2 comes back at position 3, before 4 comes back at position 5. The seen-dictionary answers "have I seen this before?" in one step, which is why the whole check is a single pass. A set does the same job; a dictionary is useful when you also want to remember something about each item, such as where you first saw it.
Pattern 3 - lookup tables and mappings
A lookup table maps an identifier to its data: users[102] is "Kiran", products["P101"] is "Mouse", countries["IN"] is "India". Use [] when the key must exist and get() when it may not - days.get(9, "Unknown day") returns the default instead of failing.
A lookup also replaces a long if/elif chain. Seven branches mapping day numbers to names become one dictionary and one get(). The data is in one place, adding a case is one line, and the logic no longer grows with the number of cases.
Pattern 4 - grouping
Grouping builds a dictionary of lists: each key holds every value that belongs to it. Create the empty list the first time a key appears, then append: if course not in groups: groups[course] = [], then groups[course].append(name). Students and courses become {‘Python’: [‘Ravi’, ‘John’], ‘Java’: [‘Kiran’, ‘Anil’]}.
groups.setdefault(course, []).append(name) does both steps in one line: setdefault returns the existing list, or inserts the empty list and returns that. The two forms give identical results; the if-version is easier to read while you are learning.
Pattern 5 - building dictionaries from lists
Two parallel lists become a dictionary with zip(): for name, age in zip(names, ages): result[name] = age gives {‘Ravi’: 30, ‘Kiran’: 28, ‘John’: 35}. dict(zip(names, ages)) is the same in one call. zip() stops at the shorter list - zip(["a", "b", "c"], [1, 2]) gives {‘a’: 1, ‘b’: 2} and silently drops "c" - so check the lengths when they must match.
A list of records becomes a lookup by id: for user in users: users_by_id[user["id"]] = user. Searching the list for id 102 is a loop every time; after one pass to build the dictionary, users_by_id[102] is a single step. This is one of the most common transformations in backend code.
Pattern 6 - maximum, minimum and sorting
To find who scored highest you track two things as you loop: the best value so far and the key it belongs to. Start them at None, not 0. A start of 0 looks harmless but breaks the moment every value is negative: on {"a": -5, "b": -2} it never finds a winner and reports None with 0.
Python has this built in: max(scores, key=scores.get) returns the key whose value is highest - "Kiran" - and min(scores, key=scores.get) the lowest. Passing scores.get as key tells max to compare by value rather than by name.
A dictionary has no sort method. sorted(scores.items(), key=lambda item: item[1]) returns a list of (key, value) pairs ordered by value - [(‘Ravi’, 80), (‘John’, 87), (‘Kiran’, 95)] - and reverse=True turns it round. Wrap it in dict() to get a dictionary back in that order. The lambda is a small inline function that picks the value out of each pair; Lesson 27 covers lambdas properly.
Pattern 7 - reverse mappings and relationships
A dictionary only looks up in one direction. To go from "India" back to "IN", build the reverse: for code, country in countries.items(): reverse[country] = code. The same idea stores any relationship - employee to manager, user to role, product to category.
Reversing is only safe when the values are unique. If two keys share a value, the later one overwrites the earlier in the reverse dictionary: reversing {"Ravi": "Python", "John": "Python", "Kiran": "Java"} gives {‘Python’: ‘John’, ‘Java’: ‘Kiran’} - Ravi is gone. When values repeat, reverse into groups instead: each value maps to a list of keys.
Watch out: A reverse mapping silently loses entries whenever two keys share a value. Count the keys before and after if you are not sure the values are unique.
Pattern 8 - validation, nesting and caching
Missing fields: loop over the required names and collect those not in the record - ["name", "email", "phone"] against a user with name and email gives [‘phone’]. This is the start of every form and API validator.
Nested data: for user_id, user in users.items() gives you each inner dictionary, and user["name"] reads inside it - outer dictionary, inner dictionary, value.
A cache remembers results you have already worked out: before computing, check if key in cache; after computing, store cache[key] = result. The second request for the same key skips the work. The idea scales all the way up to the caches in front of databases and APIs.
Choosing the pattern
Read the problem for its verb. The verb usually names the pattern.
"How many of each?"Frequency"Find it by id"Lookup table"Put these together by ..."Grouping"Have I seen this?"Seen-tracking"Map A to B" / "B back to A"Mapping / reverse mapping"Don’t compute it twice"Cache"What is missing?"ValidationSyntax and examples
numbers = [1, 2, 2, 3, 3, 3]
count = {}
for number in numbers:
count[number] = count.get(number, 0) + 1
print(count) # {1: 1, 2: 2, 3: 3}
letters = {}
for char in "banana":
letters[char] = letters.get(char, 0) + 1
print(letters) # {'b': 1, 'a': 3, 'n': 2}
sentence = "python is easy and python is powerful"
words = {}
for word in sentence.split():
words[word] = words.get(word, 0) + 1
print(words) # {'python': 2, 'is': 2, 'easy': 1, 'and': 1, 'powerful': 1}
# count[5] += 1 on a key that is not there yet -> KeyError: 5numbers = [1, 2, 3, 2, 4, 3, 5]
count = {}
for number in numbers:
count[number] = count.get(number, 0) + 1
duplicates = []
for number, frequency in count.items():
if frequency > 1:
duplicates.append(number)
print(duplicates) # [2, 3]
# The first value that repeats - stop as soon as you find it
seen = {}
for number in [4, 2, 7, 2, 9, 4, 2]:
if number in seen:
print("First duplicate:", number) # First duplicate: 2
break
seen[number] = Trueusers = {101: "Ravi", 102: "Kiran", 103: "John"}
print(users[102]) # Kiran
products = {"P100": "Laptop", "P101": "Mouse", "P102": "Keyboard"}
print(products.get("P101")) # Mouse
days = {1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday", 5: "Friday"}
print(days.get(2)) # Tuesday
print(days.get(9, "Unknown day")) # Unknown day - no if/elif chain needed
# A list of records, turned into a lookup by id
records = [
{"id": 101, "name": "Ravi"},
{"id": 102, "name": "Kiran"},
{"id": 103, "name": "John"},
]
users_by_id = {}
for record in records:
users_by_id[record["id"]] = record
print(users_by_id[102]) # {'id': 102, 'name': 'Kiran'}students = [
("Ravi", "Python"),
("Kiran", "Java"),
("John", "Python"),
("Anil", "Java"),
]
groups = {}
for name, course in students:
if course not in groups:
groups[course] = []
groups[course].append(name)
print(groups) # {'Python': ['Ravi', 'John'], 'Java': ['Kiran', 'Anil']}
# The same in one line per item
groups = {}
for name, course in students:
groups.setdefault(course, []).append(name)
print(groups) # {'Python': ['Ravi', 'John'], 'Java': ['Kiran', 'Anil']}names = ["Ravi", "Kiran", "John"]
ages = [30, 28, 35]
result = {}
for name, age in zip(names, ages):
result[name] = age
print(result) # {'Ravi': 30, 'Kiran': 28, 'John': 35}
print(dict(zip(names, ages))) # the same, in one call
print(dict(zip(["a", "b", "c"], [1, 2])))
# {'a': 1, 'b': 2} - zip stops at the shorter list; "c" is dropped silentlyscores = {"Ravi": 80, "Kiran": 95, "John": 87}
highest = None
highest_student = None
for name, score in scores.items():
if highest is None or score > highest:
highest = score
highest_student = name
print(highest_student, highest) # Kiran 95
# Built in: compare the keys by their values
print(max(scores, key=scores.get)) # Kiran
print(min(scores, key=scores.get)) # Ravi
print(sorted(scores.items(), key=lambda item: item[1]))
# [('Ravi', 80), ('John', 87), ('Kiran', 95)]
print(sorted(scores.items(), key=lambda item: item[1], reverse=True))
# [('Kiran', 95), ('John', 87), ('Ravi', 80)]
# Why not start at 0? All-negative values never beat it:
temperatures = {"a": -5, "b": -2}
best, best_key = 0, None
for key, value in temperatures.items():
if value > best:
best, best_key = value, key
print(best_key, best) # None 0 - wrongcountries = {"IN": "India", "US": "United States", "UK": "United Kingdom"}
reverse = {}
for code, country in countries.items():
reverse[country] = code
print(reverse["India"]) # IN
# Repeated values collapse when reversed
course_of = {"Ravi": "Python", "John": "Python", "Kiran": "Java"}
students_by_course = {}
for student, course in course_of.items():
students_by_course[course] = student
print(students_by_course) # {'Python': 'John', 'Java': 'Kiran'} - Ravi lost
required = ["name", "email", "phone"]
user = {"name": "Ravi", "email": "ravi@example.com"}
missing = []
for field in required:
if field not in user:
missing.append(field)
print(missing) # ['phone']
users = {"user1": {"name": "Ravi", "age": 30}, "user2": {"name": "Kiran", "age": 28}}
for user_id, details in users.items():
print(user_id, details["name"], details["age"])
# user1 Ravi 30
# user2 Kiran 28
cache = {}
for request in [10, 10]:
if request in cache:
print(request, "cached:", cache[request])
else:
cache[request] = request * request
print(request, "computed:", cache[request])
# 10 computed: 100
# 10 cached: 100students = [
{"name": "Ravi", "course": "Python", "score": 85},
{"name": "Kiran", "course": "Java", "score": 90},
{"name": "John", "course": "Python", "score": 92},
{"name": "Anil", "course": "Java", "score": 78},
]
# Group names by course
courses = {}
for student in students:
courses.setdefault(student["course"], []).append(student["name"])
print(courses) # {'Python': ['Ravi', 'John'], 'Java': ['Kiran', 'Anil']}
# Highest score
highest = None
highest_student = None
for student in students:
if highest is None or student["score"] > highest:
highest = student["score"]
highest_student = student["name"]
print(highest_student, highest) # John 92
# Average per course: group the scores, then divide
scores_by_course = {}
for student in students:
scores_by_course.setdefault(student["course"], []).append(student["score"])
for course, scores in scores_by_course.items():
print(course, sum(scores) / len(scores))
# Python 88.5
# Java 84.0The patterns
Each one is a single line inside a loop over your data.
CountHow many of each.
count[key] = count.get(key, 0) + 1
GroupEvery value under its key.
groups.setdefault(key, []).append(value)
SeenHave I met this before?
if key in seen: ... else: seen[key] = True
LookupFind by identifier.
by_id[record["id"]] = record
ReverseValue back to key - unique values only.
reverse[value] = key
CacheRemember a computed result.
cache[key] = result
MissingWhich required keys are absent.
if field not in record:
Built-ins that pair with these patterns
zip(a, b)Pairs two lists; stops at the shorter.
dict(zip(names, ages))
max(d, key=d.get)The key with the highest value.
max(scores, key=scores.get)
min(d, key=d.get)The key with the lowest value.
min(scores, key=scores.get)
sorted(d.items(), key=...)Pairs sorted by value; reverse=True for descending.
sorted(d.items(), key=lambda item: item[1])
setdefault(key, default)Return the value, inserting the default first if missing.
groups.setdefault(key, [])
str.split()Words from a sentence, ready to count.
"a b a".split()
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Replace count.get(item, 0) + 1 with count[item] += 1 and run it on a fresh dictionary. Read the error, then explain why get() fixes it.”
“Run the highest-score loop starting from 0 on {"a": -5, "b": -2}, then starting from None. Compare with max(scores, key=scores.get).”
“Reverse {"Ravi": "Python", "John": "Python"} and count the keys before and after. Then reverse it into groups instead.”
“Find the first duplicate in [4, 2, 7, 2, 9, 4]. Why is the answer 2 and not 4?”
“Zip three names with two ages and print the dictionary. Which name disappeared?”
What usually goes wrong
count[key] += 1 reads the old value first, and a new key has none - KeyError.
✗ count[number] += 1✓ count[number] = count.get(number, 0) + 1Assigning to the same key twice keeps only the last value. If a key can have several values, store a list and append.
✗ scores["Ravi"] = 30
scores["Ravi"] = 35 # 30 is gone✓ scores.setdefault("Ravi", []).append(30)
scores["Ravi"].append(35) # [30, 35]Keys are on the left of the colon. user = {"name": "Ravi"}; user["Ravi"] is a KeyError - the key is "name".
✗ user["Ravi"]✓ user["name"]If every value is negative, nothing beats 0 and the loop reports no winner. Start at None, or use max() with key.
✗ highest = 0✓ highest = None
...
if highest is None or score > highest:Later keys overwrite earlier ones in the reverse. Reverse into a dictionary of lists when values can repeat.
✗ reverse[course] = student✓ reverse.setdefault(course, []).append(student)Best practices
- Name the dictionary for what it maps: count_by_word, users_by_id, students_by_course.
- Use get(key, 0) for counting and setdefault(key, []) for grouping.
- Build a lookup dictionary once when you would otherwise search a list repeatedly.
- Prefer a dictionary to a long if/elif chain for fixed mappings.
- Start running maximums and minimums at None, or use max()/min() with key.
- Only build a plain reverse mapping when the values are unique.
- Check lengths before zip() when two lists must line up exactly.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Count each number in [1, 2, 2, 3, 3, 3, 4, 4].
Show hintHide hint
get() with a default of 0, plus one.
Show solutionHide solution
numbers = [1, 2, 2, 3, 3, 3, 4, 4]
count = {}
for number in numbers:
count[number] = count.get(number, 0) + 1
print(count) # {1: 1, 2: 2, 3: 3, 4: 2}Find every value that appears more than once in [1, 2, 3, 2, 4, 3, 5].
Show hintHide hint
Count first, then keep the keys whose count is above 1.
Show solutionHide solution
numbers = [1, 2, 3, 2, 4, 3, 5]
count = {}
for number in numbers:
count[number] = count.get(number, 0) + 1
duplicates = []
for number, frequency in count.items():
if frequency > 1:
duplicates.append(number)
print(duplicates) # [2, 3]Count every character in "programming".
Show hintHide hint
A string is iterable, so loop over it directly.
Show solutionHide solution
count = {}
for char in "programming":
count[char] = count.get(char, 0) + 1
print(count)
# {'p': 1, 'r': 2, 'o': 1, 'g': 2, 'a': 1, 'm': 2, 'i': 1, 'n': 1}Group [("Ravi", "Python"), ("Kiran", "Java"), ("John", "Python"), ("Anil", "Java")] by course.
Show hintHide hint
A dictionary of lists: create the list the first time a course appears.
Show solutionHide solution
data = [("Ravi", "Python"), ("Kiran", "Java"), ("John", "Python"), ("Anil", "Java")]
groups = {}
for name, course in data:
groups.setdefault(course, []).append(name)
print(groups) # {'Python': ['Ravi', 'John'], 'Java': ['Kiran', 'Anil']}With products = {"P101": "Laptop", "P102": "Mouse", "P103": "Keyboard"}, print the product for "P102", and a fallback for "P999".
Show hintHide hint
[] for the known id, get() with a default for the unknown one.
Show solutionHide solution
products = {"P101": "Laptop", "P102": "Mouse", "P103": "Keyboard"}
print(products["P102"]) # Mouse
print(products.get("P999", "No such product")) # No such productFind the fields from ["name", "email", "phone", "city"] that are missing from {"name": "Ravi", "email": "ravi@example.com"}.
Show hintHide hint
Loop over the required list and check each name with not in.
Show solutionHide solution
required = ["name", "email", "phone", "city"]
user = {"name": "Ravi", "email": "ravi@example.com"}
missing = []
for field in required:
if field not in user:
missing.append(field)
print(missing) # ['phone', 'city']First repeat, and a word report
Two classic interview problems, both one pass over the data with a dictionary.
- In [4, 2, 7, 2, 9, 4, 2], find the first number that appears more than once - the answer is 2.
- Explain in a comment why it is 2 and not 4.
- For "python is easy and python is powerful", count every word.
- Print the most frequent word and its count, using max() with key.
- Print the words sorted by count, highest first.
numbers = [4, 2, 7, 2, 9, 4, 2]
sentence = "python is easy and python is powerful"
# 1. First repeated number
# 2. Count the words
# 3. Most frequent word
# 4. Words by count, highest first
Show one solutionHide solution
numbers = [4, 2, 7, 2, 9, 4, 2]
sentence = "python is easy and python is powerful"
# 1. Remember each number; the first one already remembered is the answer.
# 2 comes back at index 3, before 4 comes back at index 5.
seen = {}
for number in numbers:
if number in seen:
print("First duplicate:", number) # First duplicate: 2
break
seen[number] = True
# 2. Frequency counting
count = {}
for word in sentence.split():
count[word] = count.get(word, 0) + 1
print(count) # {'python': 2, 'is': 2, 'easy': 1, 'and': 1, 'powerful': 1}
# 3. max() compares the keys by their counts; on a tie the first one seen wins
top = max(count, key=count.get)
print(top, count[top]) # python 2
# 4. Sorted pairs, highest count first
print(sorted(count.items(), key=lambda item: item[1], reverse=True))
# [('python', 2), ('is', 2), ('easy', 1), ('and', 1), ('powerful', 1)]Key points
- Most dictionary problems are one pass: read or write the dictionary at each item.
- Count with count[key] = count.get(key, 0) + 1 - never count[key] += 1 on a new key.
- Group with a dictionary of lists: setdefault(key, []).append(value).
- Use a seen-dictionary (or set) for "have I met this before?" - first duplicates in one pass.
- Turn repeated searches and if/elif chains into lookup tables.
- dict(zip(a, b)) pairs two lists, stopping at the shorter one.
- max(d, key=d.get) gives the key with the highest value; start manual maximums at None.
- sorted(d.items(), key=lambda item: item[1]) sorts by value and returns a list of pairs.
- A reverse mapping loses entries when values repeat.
- A dictionary is also a cache: check, compute once, store.
Quick check before you move on
Interview questions
How do you count the frequency of elements in a list?
One pass with a dictionary: count[x] = count.get(x, 0) + 1. In production code collections.Counter does the same and adds most_common().
Find the first repeated element in a list.
Walk the list keeping a set or dictionary of what you have seen; return the first element already in it. O(n) time, O(n) space - against O(n squared) for comparing every pair.
How do you group records by a field?
A dictionary of lists keyed by the field: groups.setdefault(record[field], []).append(record). collections.defaultdict(list) removes the setdefault call.
When is a dictionary better than an if/elif chain?
When the branches only map an input to a fixed output. The mapping becomes data - one place to read and extend - and the lookup cost does not grow with the number of cases.
What is memoization?
Caching a function’s results by its arguments, usually in a dictionary, so repeated calls return the stored answer instead of recomputing. functools.lru_cache does it for you.
Quiz
- 1.
When would you use a dictionary of lists?
- 2.
Why build users_by_id from a list of users?
- 3.
What is wrong with starting a maximum at 0?
- 4.
How do you sort a dictionary by value?
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned