Creating dictionaries
A dictionary maps keys to values. Every key is unique. Looking up a value by key takes the same time whether the dictionary has 10 items or 10 million, because dictionaries are backed by a hash table.
1# Literal syntax, most common2user = {3 "name": "Alice",4 "age": 30,5 "admin": True,6}78# dict() constructor with keyword arguments9config = dict(host="localhost", port=5432, debug=False)1011# Empty dict12cache = {}13cache = dict()1415print(user["name"]) # Alice16print(config["port"]) # 5432
dict.fromkeys()
When you need a dict with a fixed set of keys and a default value for each, dict.fromkeys() is cleaner than a loop.
1# Initialize all scores to zero2players = ["Alice", "Bob", "Carol"]3scores = dict.fromkeys(players, 0)4print(scores)5# {'Alice': 0, 'Bob': 0, 'Carol': 0}67# Initialize a config with None8fields = ["host", "port", "password"]9template = dict.fromkeys(fields)10print(template)11# {'host': None, 'port': None, 'password': None}
Do not use a mutable default like dict.fromkeys(keys, []). All keys would share the same list object. Use a dict comprehension instead: { k: [] for k in keys }.
Accessing values
Square bracket access is the direct way to read a value. It is fast and readable. The cost is that it raises a KeyError if the key does not exist. When you are not sure if a key is present, .get() is safer.
1config = {"host": "localhost", "port": 5432}23# Direct access: fast, but raises KeyError if missing4print(config["host"]) # localhost56# .get(): returns None if key is absent7print(config.get("user")) # None8print(config.get("user", "root")) # root (custom default)910# Check membership before accessing11if "port" in config:12 print(config["port"]) # 54321314# KeyError example15config["missing"] # KeyError: 'missing'
setdefault()
.setdefault(key, default) returns the value if the key exists and inserts the default if it does not. It is the one-liner for the common pattern of "give me the value, or insert and return this default."
1# Building a grouped dict without setdefault2groups = {}3items = [("fruit", "apple"), ("veg", "carrot"), ("fruit", "banana")]45for category, item in items:6 if category not in groups:7 groups[category] = []8 groups[category].append(item)910# With setdefault: same result, less code11groups = {}12for category, item in items:13 groups.setdefault(category, []).append(item)1415print(groups)16# {'fruit': ['apple', 'banana'], 'veg': ['carrot']}
Try the lookup modes below. Keys marked with ? are not in the dict. Switch between d["key"] and d.get("key") to see the difference.
Modifying dictionaries
Dicts are mutable. Assignment adds or updates a key. Deletion removes one. These operations are all O(1).
1user = {"name": "Alice", "age": 30}23# Add a new key4user["email"] = "alice@example.com"56# Update an existing key7user["age"] = 3189# Delete a key10del user["email"]1112# pop() removes and returns the value13age = user.pop("age")14print(age) # 3115print(user) # {'name': 'Alice'}1617# pop() with a default avoids KeyError18role = user.pop("role", "viewer")19print(role) # viewer
Merging dicts
Python 3.9 introduced the | operator for merging two dicts into a new one. The |= operator updates in place. For older Python, use .update(). In all cases, the right-hand dict wins on key conflicts.
1defaults = {"theme": "dark", "lang": "en", "timeout": 30}2overrides = {"lang": "fr", "timeout": 60}34# | creates a new dict (Python 3.9+)5config = defaults | overrides6print(config)7# {'theme': 'dark', 'lang': 'fr', 'timeout': 60}89# .update() modifies in place (all Python versions)10defaults.update(overrides)11print(defaults)12# {'theme': 'dark', 'lang': 'fr', 'timeout': 60}1314# Original defaults are modified, use | if you need to preserve them
Useful methods
1d = {"a": 1, "b": 2, "c": 3}23# popitem() removes and returns the last inserted item4key, val = d.popitem()5print(key, val) # c 367# clear() empties the dict8d.clear()9print(d) # {}1011# copy() creates a shallow copy12original = {"x": [1, 2]}13shallow = original.copy()14shallow["x"].append(3)15print(original) # {'x': [1, 2, 3]}: inner list is shared
Iterating
Dictionaries have three view objects: .keys(), .values(), and .items(). They are live, they reflect any changes to the dict without being rebuilt. Iterating a dict directly gives you its keys, same as .keys().
1scores = {"Alice": 92, "Bob": 78, "Carol": 85}23# Keys (default)4for name in scores:5 print(name) # Alice, Bob, Carol67# Values8for score in scores.values():9 print(score) # 92, 78, 851011# Both: .items() is the most common loop12for name, score in scores.items():13 print(f"{name}: {score}")
Sorted iteration
Python 3.7+ guarantees that dicts preserve insertion order. If you need a different order, wrap the iteration in sorted(). The dict itself is never modified.
1scores = {"Alice": 92, "Bob": 78, "Carol": 85}23# Sort by name (alphabetical)4for name, score in sorted(scores.items()):5 print(f"{name}: {score}")67print()89# Sort by score (descending)10for name, score in sorted(scores.items(), key=lambda item: item[1], reverse=True):11 print(f"{name}: {score}")
Never modify during iteration
Adding or removing keys while iterating raises a RuntimeError. If you need to filter a dict, iterate a copy of the keys or build a new dict.
1scores = {"Alice": 92, "Bob": 45, "Carol": 85, "Dan": 38}23# Remove failing scores: iterate a snapshot of keys4for name in list(scores.keys()):5 if scores[name] < 50:6 del scores[name]78print(scores) # {'Alice': 92, 'Carol': 85}910# Or build a new dict with a comprehension (cleaner)11passing = {name: s for name, s in scores.items() if s >= 50}12print(passing) # {'Alice': 92, 'Carol': 85}
Dict comprehensions
A dict comprehension builds a new dictionary in a single expression, the same way a list comprehension builds a list. The syntax is { key: value for item in iterable }.
1# Square each number2squares = {n: n ** 2 for n in range(6)}3print(squares)4# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}56# Map a list to a dict7names = ["Alice", "Bob", "Carol"]8lengths = {name: len(name) for name in names}9print(lengths)10# {'Alice': 5, 'Bob': 3, 'Carol': 5}
With a filter condition
1scores = {"Alice": 92, "Bob": 45, "Carol": 85, "Dan": 38}23# Keep only passing scores4passing = {name: s for name, s in scores.items() if s >= 50}5print(passing)6# {'Alice': 92, 'Carol': 85}
Inverting a dict
Swapping keys and values is a one-liner with a comprehension. It only works correctly when all values are unique and hashable.
1country_capital = {2 "France": "Paris",3 "Germany": "Berlin",4 "Japan": "Tokyo",5}67capital_country = {v: k for k, v in country_capital.items()}8print(capital_country)9# {'Paris': 'France', 'Berlin': 'Germany', 'Tokyo': 'Japan'}1011print(capital_country["Tokyo"]) # Japan
Building from two lists
1keys = ["host", "port", "user"]2values = ["localhost", 5432, "admin"]34config = {k: v for k, v in zip(keys, values)}5print(config)6# {'host': 'localhost', 'port': 5432, 'user': 'admin'}78# dict(zip(...)) is equivalent and slightly shorter9config = dict(zip(keys, values))10print(config)11# {'host': 'localhost', 'port': 5432, 'user': 'admin'}
defaultdict and Counter
The collections module ships two dict subclasses that handle the most common dict patterns with less boilerplate.
defaultdict
A defaultdict never raises a KeyError. When you access a missing key, it calls a factory function to create the default value and inserts it automatically. The factory is any callable: list, int, set, or a lambda.
1from collections import defaultdict23# Group items by category4items = [("fruit", "apple"), ("veg", "carrot"), ("fruit", "banana")]56# Without defaultdict: requires setdefault or an if-check7groups = {}8for cat, item in items:9 groups.setdefault(cat, []).append(item)1011# With defaultdict: cleaner12groups = defaultdict(list)13for cat, item in items:14 groups[cat].append(item) # no KeyError, list created automatically1516print(dict(groups))17# {'fruit': ['apple', 'banana'], 'veg': ['carrot']}1819# defaultdict(int) for counting20text = "hello world"21freq = defaultdict(int)22for char in text:23 freq[char] += 1 # starts at 0 automatically2425print(dict(freq))
Counter
Counter is a dict subclass built for counting. Pass it any iterable and it tallies the occurrences of each element. The .most_common(n) method returns the top n items sorted by frequency.
1from collections import Counter23words = "the cat sat on the mat the cat".split()4c = Counter(words)56print(c)7# Counter({'the': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1})89print(c.most_common(2))10# [('the', 3), ('cat', 2)]1112print(c["cat"]) # 213print(c["dog"]) # 0: missing keys return 0, no KeyError1415# Counters support arithmetic16a = Counter("aab")17b = Counter("abc")18print(a + b) # Counter({'a': 3, 'b': 2, 'c': 1})19print(a - b) # Counter({'a': 1}): removes non-positive counts
See Counter in action below. Pick a text and toggle between word and character frequency.
Real-world patterns
Grouping records
The most common dict pattern: start with a flat list, group it by some attribute. A defaultdict(list) or setdefault both work.
1from collections import defaultdict23employees = [4 {"name": "Alice", "dept": "Engineering"},5 {"name": "Bob", "dept": "Design"},6 {"name": "Carol", "dept": "Engineering"},7 {"name": "Dan", "dept": "Design"},8]910by_dept = defaultdict(list)11for e in employees:12 by_dept[e["dept"]].append(e["name"])1314print(dict(by_dept))15# {'Engineering': ['Alice', 'Carol'], 'Design': ['Bob', 'Dan']}
Memoization / caching
Store results you have already computed so you never redo expensive work. A dict keyed by the function's arguments is the simplest cache.
1def fib(n: int, cache: dict = {}) -> int:2 if n in cache:3 return cache[n]4 if n <= 1:5 return n6 result = fib(n - 1) + fib(n - 2)7 cache[n] = result8 return result910print([fib(i) for i in range(10)])11# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]1213# functools.lru_cache does this automatically and is preferred14from functools import lru_cache1516@lru_cache(maxsize=None)17def fib2(n: int) -> int:18 if n <= 1:19 return n20 return fib2(n - 1) + fib2(n - 2)
Safe nested access
Deeply nested dicts are common in API responses. Chaining .get() avoids KeyError at every level.
1response = {2 "user": {3 "profile": {4 "city": "Berlin",5 }6 }7}89# Unsafe: any missing key raises KeyError10# city = response["user"]["profile"]["city"]1112# Safe: returns None at the first missing key13city = (response14 .get("user", {})15 .get("profile", {})16 .get("city"))1718print(city) # Berlin1920missing = (response21 .get("user", {})22 .get("address", {})23 .get("zip"))2425print(missing) # None
Dispatch table
Replace long if-elif chains with a dict that maps keys to callables. Cleaner, easier to extend, and lookups are O(1).
1def handle_create(data): return f"created {data}"2def handle_update(data): return f"updated {data}"3def handle_delete(data): return f"deleted {data}"45handlers = {6 "CREATE": handle_create,7 "UPDATE": handle_update,8 "DELETE": handle_delete,9}1011def dispatch(action: str, data: str) -> str:12 handler = handlers.get(action)13 if handler is None:14 return f"unknown action: {action}"15 return handler(data)1617print(dispatch("CREATE", "user-42")) # created user-4218print(dispatch("DELETE", "post-7")) # deleted post-719print(dispatch("RESET", "db")) # unknown action: RESET