← Back to Core Python
Lesson 20 · Python Collections

Dictionary Patterns

Counting, grouping, lookups, reverse maps, caches - the handful of dictionary patterns behind most data problems.

Beginner50 min

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.

Counting [1, 2, 2] step by step
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.

Problem -> 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?"Validation

Syntax and examples

Frequency counting - numbers, characters, words
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: 5
Duplicates - all of them, and the first one
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] # 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] = True
Lookup tables instead of searching and if/elif
users = {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'}
Grouping - a dictionary of lists
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']}
Building from two lists with zip()
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 silently
Highest, lowest, and sorting by value
scores = {"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 - wrong
Reverse lookups, missing fields, nesting, caching
countries = {"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: 100
The worked example - student analysis
students = [ {"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.0

The patterns

Each one is a single line inside a loop over your data.

Count

How many of each.

count[key] = count.get(key, 0) + 1
Group

Every value under its key.

groups.setdefault(key, []).append(value)
Seen

Have I met this before?

if key in seen: ... else: seen[key] = True
Lookup

Find by identifier.

by_id[record["id"]] = record
Reverse

Value back to key - unique values only.

reverse[value] = key
Cache

Remember a computed result.

cache[key] = result
Missing

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

Break the counter

“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.”

Negative scores

“Run the highest-score loop starting from 0 on {"a": -5, "b": -2}, then starting from None. Compare with max(scores, key=scores.get).”

Lose a key

“Reverse {"Ravi": "Python", "John": "Python"} and count the keys before and after. Then reverse it into groups instead.”

First duplicate

“Find the first duplicate in [4, 2, 7, 2, 9, 4]. Why is the answer 2 and not 4?”

zip with uneven lists

“Zip three names with two ages and print the dictionary. Which name disappeared?”

What usually goes wrong

Incrementing a key that does not exist yet

count[key] += 1 reads the old value first, and a new key has none - KeyError.

✗ count[number] += 1
✓ count[number] = count.get(number, 0) + 1
Overwriting when you meant to collect

Assigning 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]
Looking up by the value

Keys are on the left of the colon. user = {"name": "Ravi"}; user["Ravi"] is a KeyError - the key is "name".

✗ user["Ravi"]
✓ user["name"]
Starting a maximum at 0

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:
Reversing a mapping with repeated values

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.

1.

Count each number in [1, 2, 2, 3, 3, 3, 4, 4].

Show hint

get() with a default of 0, plus one.

Show 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}
2.

Find every value that appears more than once in [1, 2, 3, 2, 4, 3, 5].

Show hint

Count first, then keep the keys whose count is above 1.

Show 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]
3.

Count every character in "programming".

Show hint

A string is iterable, so loop over it directly.

Show 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}
4.

Group [("Ravi", "Python"), ("Kiran", "Java"), ("John", "Python"), ("Anil", "Java")] by course.

Show hint

A dictionary of lists: create the list the first time a course appears.

Show 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']}
5.

With products = {"P101": "Laptop", "P102": "Mouse", "P103": "Keyboard"}, print the product for "P102", and a fallback for "P999".

Show hint

[] for the known id, get() with a default for the unknown one.

Show solution
products = {"P101": "Laptop", "P102": "Mouse", "P103": "Keyboard"} print(products["P102"]) # Mouse print(products.get("P999", "No such product")) # No such product
6.

Find the fields from ["name", "email", "phone", "city"] that are missing from {"name": "Ravi", "email": "ravi@example.com"}.

Show hint

Loop over the required list and check each name with not in.

Show 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']
Coding challenge

First repeat, and a word report

Two classic interview problems, both one pass over the data with a dictionary.

It should
  • 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 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

After counting [1, 2, 2, 3, 3, 3] with get(), what is count[3]?
3.
Why does count[key] += 1 fail on an empty dictionary?
It has to read the current value first, and a new key has none - KeyError.
What is the first duplicate in [4, 2, 7, 2, 9, 4]?
2 - it reappears at index 3, before 4 reappears at index 5.
What does dict(zip(["a", "b", "c"], [1, 2])) give?
{‘a’: 1, ‘b’: 2} - zip stops at the shorter list.
Which key does max(scores, key=scores.get) return for {"Ravi": 80, "Kiran": 95}?
"Kiran" - the key with the highest value.
What happens when you reverse {"Ravi": "Python", "John": "Python"}?
You get {‘Python’: ‘John’} - Ravi is overwritten, because both keys share the value.

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

    When would you use a dictionary of lists?

  2. 2.

    Why build users_by_id from a list of users?

  3. 3.

    What is wrong with starting a maximum at 0?

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