The collections module provides specialized dictionary types: defaultdict for automatic default values, Counter for counting occurrences, OrderedDict for preserving insertion order (though regular dicts now do this too), and ChainMap for combining multiple dictionaries. You can also create dictionaries with dictionary comprehensions, merge them with the | operator, and use advanced lookup techniques.
This lesson explores these advanced dictionary capabilities. You will learn to count items efficiently, create nested dictionaries automatically, combine multiple dictionaries, and use dictionaries for sophisticated data processing.
🕯️ Magic Note
Starting from Python 3.7, regular dictionaries preserve insertion order. This was an implementation detail in 3.6 and became a language guarantee in 3.7. OrderedDict is still useful for methods like move_to_end() that regular dicts do not have.
Python
from collections import defaultdict
# Without defaultdict (manual checking)
word_count = {}
words = [“apple”, “banana”, “apple”, “cherry”, “banana”, “apple”]
for word in words:
if word not in word_count:
word_count[word] = 0
word_count[word] += 1
# With defaultdict (automatic default)
word_count = defaultdict(int) # int() returns 0
for word in words:
word_count[word] += 1
print(dict(word_count)) # {‘apple’: 3, ‘banana’: 2, ‘cherry’: 1}
# defaultdict with list (grouping items)
groups = defaultdict(list)
students = [(“A”, “Ali”), (“B”, “Sara”), (“A”, “Reza”), (“C”, “Mina”), (“B”, “Hassan”)]
for grade, name in students:
groups[grade].append(name)
print(dict(groups)) # {‘A’: [‘Ali’, ‘Reza’], ‘B’: [‘Sara’, ‘Hassan’], ‘C’: [‘Mina’]}
# defaultdict with set (unique grouping)
unique_groups = defaultdict(set)
for grade, name in students:
unique_groups[grade].add(name)
print(dict(unique_groups)) # {‘A’: {‘Ali’, ‘Reza’}, ‘B’: {‘Sara’, ‘Hassan’}, ‘C’: {‘Mina’}}
Python
from collections import Counter
# Create counter from iterable
words = [“apple”, “banana”, “apple”, “cherry”, “banana”, “apple”]
counter = Counter(words)
print(counter) # Counter({‘apple’: 3, ‘banana’: 2, ‘cherry’: 1})
# Create counter from string
char_counter = Counter(“mississippi”)
print(char_counter) # Counter({‘i’: 4, ‘s’: 4, ‘p’: 2, ‘m’: 1})
# Most common elements
print(counter.most_common(2)) # [(‘apple’, 3), (‘banana’, 2)]
# Counter arithmetic
c1 = Counter(a=3, b=2, c=1)
c2 = Counter(a=1, b=2, d=1)
print(c1 + c2) # Counter({‘a’: 4, ‘b’: 4, ‘c’: 1, ‘d’: 1})
print(c1 – c2) # Counter({‘a’: 2, ‘c’: 1}) (keeps only positive counts)
print(c1 & c2) # Counter({‘b’: 2, ‘a’: 1}) (intersection – min)
print(c1 | c2) # Counter({‘a’: 3, ‘b’: 2, ‘c’: 1, ‘d’: 1}) (union – max)
# Update counter
counter.update([“apple”, “banana”])
print(counter) # Counter({‘apple’: 4, ‘banana’: 3, ‘cherry’: 1})
🕯️ Magic Note
Counter is perfect for frequency analysis, word counting, inventory management, and any situation where you need to count occurrences. It is also very fast because it is implemented in C.
Python
from collections import OrderedDict
# Create OrderedDict (order preserved)
od = OrderedDict()
od[“a”] = 1
od[“b”] = 2
od[“c”] = 3
print(od) # OrderedDict([(‘a’, 1), (‘b’, 2), (‘c’, 3)])
# move_to_end() – move key to end (or beginning)
od.move_to_end(“a”)
print(od) # OrderedDict([(‘b’, 2), (‘c’, 3), (‘a’, 1)])
od.move_to_end(“b”, last=False) # Move to beginning
print(od) # OrderedDict([(‘b’, 2), (‘c’, 3), (‘a’, 1)])
# popitem() – pop last (or first) item
last = od.popitem(last=True) # Remove and return last item
first = od.popitem(last=False) # Remove and return first item
print(last, first)
# Equality is order-sensitive (unlike regular dicts)
d1 = OrderedDict([(“a”, 1), (“b”, 2)])
d2 = OrderedDict([(“b”, 2), (“a”, 1)])
print(d1 == d2) # False (order matters)
d3 = {“a”: 1, “b”: 2}
d4 = {“b”: 2, “a”: 1}
print(d3 == d4) # True (order does not matter for regular dicts)
🕯️ Magic Note
Use OrderedDict when you need order-sensitive equality or when you need to move items to the beginning or end frequently. For most other use cases, regular dicts are sufficient.
Python
from collections import ChainMap
defaults = {“color”: “black”, “font”: “Arial”, “size”: 12}
user_prefs = {“color”: “blue”, “size”: 14}
# ChainMap searches in order (first match wins)
settings = ChainMap(user_prefs, defaults)
print(settings[“color”]) # ‘blue’ (from user_prefs)
print(settings[“font”]) # ‘Arial’ (from defaults)
print(settings[“size”]) # 14 (from user_prefs)
# Update ChainMap (only affects first dictionary)
settings[“size”] = 16
print(user_prefs[“size”]) # 16
print(defaults[“size”]) # 12 (unchanged)
# Add new dictionary to the front
cmd_args = {“color”: “red”}
settings = settings.new_child(cmd_args)
print(settings[“color”]) # ‘red’ (from cmd_args)
# Get all maps
print(settings.maps) # [{‘color’: ‘red’}, {‘color’: ‘blue’, ‘size’: 16}, {‘color’: ‘black’, ‘font’: ‘Arial’, ‘size’: 12}]
🕯️ Magic Note
ChainMap is perfect for managing configuration with fallbacks: command line arguments → user settings → system defaults. It avoids copying dictionaries, making it memory efficient for large nested configurations.
Python
# Basic dictionary comprehension
squares = {x: x**2 for x in range(1, 6)}
print(squares) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# Dictionary comprehension with condition
even_squares = {x: x**2 for x in range(1, 11) if x % 2 == 0}
print(even_squares) # {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}
# Transform existing dictionary
original = {“a”: 1, “b”: 2, “c”: 3}
doubled = {k: v * 2 for k, v in original.items()}
print(doubled) # {‘a’: 2, ‘b’: 4, ‘c’: 6}
# Swap keys and values
swapped = {v: k for k, v in original.items()}
print(swapped) # {1: ‘a’, 2: ‘b’, 3: ‘c’}
# Nested dictionary comprehension
matrix = {i: {j: i * j for j in range(1, 4)} for i in range(1, 4)}
print(matrix) # {1: {1: 1, 2: 2, 3: 3}, 2: {1: 2, 2: 4, 3: 6}, 3: {1: 3, 2: 6, 3: 9}}
# Dictionary comprehension with conditional expression
numbers = [1, 2, 3, 4, 5]
labels = {x: “even” if x % 2 == 0 else “odd” for x in numbers}
print(labels) # {1: ‘odd’, 2: ‘even’, 3: ‘odd’, 4: ‘even’, 5: ‘odd’}
Python
dict1 = {“a”: 1, “b”: 2}
dict2 = {“c”: 3, “d”: 4}
dict3 = {“a”: 10, “e”: 5}
# Merge with | (creates new dictionary)
merged = dict1 | dict2
print(merged) # {‘a’: 1, ‘b’: 2, ‘c’: 3, ‘d’: 4}
# Later values override earlier ones
merged = dict1 | dict3
print(merged) # {‘a’: 10, ‘b’: 2, ‘e’: 5}
# In-place merge (updates dict1)
dict1 |= dict2
print(dict1) # {‘a’: 1, ‘b’: 2, ‘c’: 3, ‘d’: 4}
# For older Python versions, use {**dict1, **dict2}
merged_old = {**dict1, **dict2}
print(merged_old) # {‘a’: 1, ‘b’: 2, ‘c’: 3, ‘d’: 4}
Python
d = {“a”: 1, “b”: 2, “c”: 3}
keys = d.keys()
values = d.values()
items = d.items()
print(f”Keys: {keys}”) # dict_keys([‘a’, ‘b’, ‘c’])
print(f”Values: {values}”) # dict_values([1, 2, 3])
print(f”Items: {items}”) # dict_items([(‘a’, 1), (‘b’, 2), (‘c’, 3)])
# Views are dynamic (reflect changes)
d[“d”] = 4
print(f”After adding: {keys}”) # dict_keys([‘a’, ‘b’, ‘c’, ‘d’])
# Convert to list if needed
keys_list = list(keys)
# Set-like operations on keys view
d1 = {“a”: 1, “b”: 2, “c”: 3}
d2 = {“b”: 20, “c”: 30, “d”: 40}
print(d1.keys() & d2.keys()) # {‘b’, ‘c’}
print(d1.keys() – d2.keys()) # {‘a’}
print(d1.keys() | d2.keys()) # {‘a’, ‘b’, ‘c’, ‘d’}
Python
d = {“a”: 1, “b”: 2}
# get() with default value
print(d.get(“a”, 0)) # 1
print(d.get(“c”, 0)) # 0
# setdefault() – get value if key exists, otherwise set and return default
counts = {}
words = [“a”, “b”, “a”, “c”, “b”, “a”]
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts) # {‘a’: 3, ‘b’: 2, ‘c’: 1}
# setdefault for nested dictionaries
nested = {}
nested.setdefault(“a”, {})[“b”] = 1
print(nested) # {‘a’: {‘b’: 1}}
# Better: use defaultdict for nested structures
from collections import defaultdict
nested = defaultdict(dict)
nested[“a”][“b”] = 1
print(dict(nested)) # {‘a’: {‘b’: 1}}
Python
from collections import Counter, defaultdict
import re
def analyze_text(text):
“””Analyze word frequency and patterns in text.”””
# Clean and split text
words = re.findall(r”\b\w+\b”, text.lower())
# Word frequency
word_freq = Counter(words)
# Words by length
by_length = defaultdict(list)
for word in set(words):
by_length[len(word)].append(word)
# First letter frequency
first_letter = Counter(word[0] for word in words if word)
# Most common words
most_common = word_freq.most_common(10)
return {
“total_words”: len(words),
“unique_words”: len(word_freq),
“word_frequency”: word_freq,
“most_common”: most_common,
“words_by_length”: dict(by_length),
“first_letter_freq”: first_letter
}
sample = “The quick brown fox jumps over the lazy dog. The dog barks quickly.”
results = analyze_text(sample)
print(f”Total words: {results[‘total_words’]}”)
print(f”Unique words: {results[‘unique_words’]}”)
print(f”Most common: {results[‘most_common’]}”)
print(f”Words by length: {results[‘words_by_length’]}”)
Python
from collections import ChainMap, defaultdict
class ConfigManager:
def __init__(self):
self._system_defaults = {
“host”: “localhost”,
“port”: 8080,
“debug”: False,
“timeout”: 30
}
self._user_config = {}
self._env_config = {}
self._cli_args = {}
self._config = ChainMap(self._cli_args, self._env_config, self._user_config, self._system_defaults)
def set_user_config(self, key, value):
self._user_config[key] = value
def set_env_config(self, key, value):
self._env_config[key] = value
def set_cli_args(self, key, value):
self._cli_args[key] = value
def get(self, key, default=None):
return self._config.get(key, default)
def get_all(self):
return dict(self._config)
config = ConfigManager()
config.set_user_config(“port”, 3000)
config.set_env_config(“debug”, True)
config.set_cli_args(“host”, “192.168.1.100”)
print(f”Host: {config.get(‘host’)}”) # 192.168.1.100 (CLI overrides)
print(f”Port: {config.get(‘port’)}”) # 3000 (user config)
print(f”Debug: {config.get(‘debug’)}”) # True (env)
print(f”Timeout: {config.get(‘timeout’)}”) # 30 (default)
print(f”All: {config.get_all()}”)
- Assuming defaultdict works with keys that do not exist for reading (d[“missing”] still raises KeyError; only assignment works)
- Forgetting that Counter.most_common() returns a list of tuples, not a Counter
- Using regular dict when order matters across Python versions (use OrderedDict for pre-3.7 compatibility)
- Modifying a dictionary while iterating over its items (can cause RuntimeError)
- Expecting ChainMap to copy dictionaries (it does not; it references them)
- Write a function that groups a list of words by their first letter using defaultdict.
- What is the difference between Counter and defaultdict(int)?
- How do you merge two dictionaries in Python 3.9+?
- Write a dictionary comprehension that creates a mapping from numbers to their cubes for numbers 1 to 10.
- What is the purpose of ChainMap? Give a use case.
- How do you get the first item from an OrderedDict?
⚡ Whisper
Dictionaries are the memory of Python. Every key points to a value. defaultdict ensures the key always has a home. Counter tallies the world. OrderedDict remembers the order of arrival. ChainMap layers configuration. Each tool is a specialized dictionary. Each solves a specific problem. Use defaultdict for grouping and counting. Use Counter for frequency analysis. Use OrderedDict when order-sensitive equality matters. Use ChainMap for cascading lookups. Master them, and your dictionaries become not just storage, but logic. They compute. They organize. They prioritize. The data is in the keys and values. The intelligence is in choosing the right dictionary. Choose wisely. Your code will be shorter, faster, and clearer.