Dictionaries.
Data with names.
A list indexes by number — items[0]. A dictionary indexes by name — user["email"]. That one change turns a jumble of values into self-describing data. Every JSON API response is a dict. Every config file becomes a dict. Every database row, every user profile, every settings panel — dicts are how Python models real-world objects before you need classes.
A Python dictionary is a collection of key-value pairs written as {"name": "Alice", "age": 30}. Access values by key: user["name"]. Add or update by assignment: user["age"] = 31. The two most important rules: use .get() when a key might not exist (avoids KeyError), and use .items() to loop over keys and values together. Dictionaries are mutable and (since Python 3.7) preserve insertion order.
Before this lesson: you should be comfortable with lists, loops, and functions. If not, read Lesson 2, Lesson 3, and Lesson 4 first.
📖 Basics — create, read, write, delete
The four operations you'll use on every dict, every day.
# Create a dict — curly braces, key: value pairs
user = {
"name": "Alice",
"age": 30,
"email": "alice@example.com",
}
# Or with dict() constructor
user2 = dict(name="Bob", age=25)
# Or an empty dict
empty = {}
# READ — access by key
print(user["name"]) # Alice
print(user["age"]) # 30
# WRITE — add or update
user["age"] = 31 # update existing key
user["city"] = "NYC" # add new key
# DELETE
del user["city"]
removed = user.pop("email") # remove and return value
# CHECK — is a key present?
print("name" in user) # True
print("phone" in user) # False
# SIZE
print(len(user)) # 2 (name, age)
Keys are unique
Each key appears once. Assigning to an existing key overwrites the value. Two keys with the same name would collide — Python keeps the last one.
Keys must be hashable
Strings, numbers, tuples, booleans — yes. Lists, dicts, sets — no (TypeError: unhashable type). This is why user[[1, 2]] doesn't work.
Insertion order preserved
Since Python 3.7, dicts remember the order you added keys. {"a": 1, "b": 2} iterates a then b. Pre-3.7 the order was random.
No index access
user[0] doesn't exist — you can't index a dict by position. Use list(user.keys())[0] if you truly need the first key, but usually you don't.
🛡️ .get() — the safety net
The single method that prevents most dict bugs.
If you access a missing key with user["phone"], Python raises KeyError and crashes. Use .get() instead — it returns None if the key is missing, or a default value you provide. Use .get() whenever the key might not exist. Use [] only when a missing key is genuinely a bug you want to catch.
user = {"name": "Alice", "age": 30}
# Using [] — raises KeyError if missing
print(user["name"]) # Alice
# print(user["phone"]) → KeyError: 'phone'
# Using .get() — returns None if missing
print(user.get("name")) # Alice
print(user.get("phone")) # None (no crash)
# Using .get() with a default
print(user.get("phone", "N/A")) # N/A
print(user.get("age", 0)) # 30
# Check-then-access pattern (works but verbose)
if "phone" in user:
print(user["phone"])
# Cleaner with .get()
if user.get("phone"):
print(user["phone"])
# The classic bug: counting with dict
counts = {}
words = ["apple", "banana", "apple", "cherry", "apple"]
# Without .get() — crashes on first missing key
# for w in words:
counts[w] = counts[w] + 1 → KeyError
# With .get() — clean
for w in words:
counts[w] = counts.get(w, 0) + 1
print(counts) # {'apple': 3, 'banana': 1, 'cherry': 1}
The .get(0) + 1 pattern is everywhere. When you're counting frequencies, accumulating totals, or grouping items, counts[key] = counts.get(key, 0) + 1 is the idiom. It reads as "get the current count, or 0 if this is the first one, then add 1." Master this pattern — you'll write it hundreds of times.
🔧 The eight methods you need
Everything else is documentation lookup.
user = {"name": "Alice", "age": 30}
# .keys() — all keys (returns a view, not a list)
print(user.keys()) # dict_keys(['name', 'age'])
print(list(user.keys())) # ['name', 'age']
# .values() — all values
print(user.values()) # dict_values(['Alice', 30])
# .items() — (key, value) pairs
print(user.items()) # dict_items([('name', 'Alice'), ('age', 30)])
# .get(key, default) — access safely
print(user.get("email", "N/A")) # N/A
# .pop(key, default) — remove and return
age = user.pop("age") # 30, and removes "age" from user
missing = user.pop("x", "gone") # "gone" — no KeyError
# .setdefault(key, default) — get or set if missing
user.setdefault("role", "user") # adds "role": "user"
user.setdefault("name", "XXX") # does nothing — "name" exists
# .update(other_dict) — merge
user.update({"email": "a@b.com", "age": 31})
# .clear() — empty the dict
user.clear()
.keys(), .values(), .items() return views, not lists. You can't index them or slice them. If you need actual lists, wrap: list(user.keys()). Views are live — if you modify the dict, the view reflects it. This is why for k in user.keys(): user[new_k] = ... raises "dictionary changed size during iteration".
.setdefault() is underused. If you're building a dict-of-lists and writing if k not in d: d[k] = [], replace that with d.setdefault(k, []).append(item). One line, no double lookup, no verbose check. This is the cleanest pattern for grouping data.
🔁 Iterating — the four ways
You will loop over dicts more than any other structure.
scores = {"Alice": 92, "Bob": 85, "Charlie": 98}
# 1. Loop over keys (default)
for name in scores:
print(name)
# Alice, Bob, Charlie
# 2. Loop over values
for score in scores.values():
print(score)
# 92, 85, 98
# 3. Loop over (key, value) pairs — the most useful
for name, score in scores.items():
print(f"{name}: {score}")
# Alice: 92
# Bob: 85
# Charlie: 98
# 4. Loop in sorted order
for name in sorted(scores):
print(name, scores[name])
# Alice 92
# Bob 85
# Charlie 98
# Sort by value (highest scores first)
for name, score in sorted(scores.items(), key=lambda kv: kv[1], reverse=True):
print(f"{name}: {score}")
# Charlie: 98
# Alice: 92
# Bob: 85
Default is keys
for x in mydict: iterates keys, not items. If you want both, use .items(). This trips up everyone once.
Order follows insertion
Python 3.7+ keeps the order you inserted keys. {"b": 1, "a": 2} iterates b then a — not sorted, but predictable.
Sort by key
sorted(mydict) or sorted(mydict.items()) sorts by key (alphabetically for strings, numerically for ints).
Sort by value
sorted(mydict.items(), key=lambda kv: kv[1]). Add reverse=True for descending. This is the most common sort you'll write.
🏗️ Nested dicts — dicts inside dicts
Real data is rarely flat. Real data is nested.
Every JSON API response you'll ever consume is a dict containing dicts containing lists containing dicts. Learning to navigate nested structures is the moment you stop needing tutorials for every new data format.
# A realistic nested structure
user = {
"name": "Alice",
"address": {
"street": "123 Main St",
"city": "NYC",
"zip": "10001",
},
"orders": [
{"id": 1, "total": 49.99},
{"id": 2, "total": 129.50},
],
}
# Access nested values by chaining
print(user["address"]["city"]) # NYC
print(user["orders"][0]["total"]) # 49.99
# Safe access with .get() chained
city = user.get("address", {}).get("city", "Unknown")
print(city) # NYC
phone = user.get("phone", {}).get("number", "N/A")
print(phone) # N/A (no crash)
# Add to nested structures
user["address"]["country"] = "USA"
user["orders"].append({"id": 3, "total": 75.00})
# Sum all order totals
total = sum(order["total"] for order in user["orders"])
print(f"Total: latex
{total:.2f}"</span>) <span class="com"># Total:
254.49
# Dict of dicts — common for lookups
users = {
"alice": {"age": 30, "role": "admin"},
"bob": {"age": 25, "role": "user"},
}
print(users["alice"]["role"]) # admin
Chain .get() with empty dict defaults. user.get("address", {}).get("city") — if "address" is missing, the outer .get() returns {}, and the inner .get() on the empty dict returns None instead of crashing. This one pattern handles 90% of "is this nested key present?" questions without a try/except block.
⚡ Dict comprehensions
Build a dict in one line. Filter, transform, or invert.
# Basic: squares
squares = {x: x**2 for x in range(5)}
print(squares) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# Filter with if
even_squares = {x: x**2 for x in range(10) if x % 2 == 0}
print(even_squares) # {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
# Invert a dict (swap keys and values)
scores = {"Alice": 92, "Bob": 85, "Charlie": 98}
by_score = {score: name for name, score in scores.items()}
print(by_score) # {92: 'Alice', 85: 'Bob', 98: 'Charlie'}
# Transform values
doubled = {name: score * 2 for name, score in scores.items()}
print(doubled) # {'Alice': 184, 'Bob': 170, 'Charlie': 196}
# From two lists
names = ["a", "b", "c"]
nums = [1, 2, 3]
combined = {k: v for k, v in zip(names, nums)}
print(combined) # {'a': 1, 'b': 2, 'c': 3}
# Group a list by a property
people = [
{"name": "Alice", "dept": "eng"},
{"name": "Bob", "dept": "sales"},
{"name": "Charlie", "dept": "eng"},
]
by_dept = {}
for p in people:
by_dept.setdefault(p["dept"], []).append(p["name"])
print(by_dept) # {'eng': ['Alice', 'Charlie'], 'sales': ['Bob']}
Comprehension vs loop — when to use which. If the logic is one expression per item, use a comprehension. If you need multiple statements, conditional logic beyond a single if, or you're calling .setdefault() / .append(), use a regular loop. Don't force comprehensions — they're a tool, not a requirement.
💻 A complete example
Real code that uses dicts the way they're used in the wild.
# word_stats.py — analyze text with dicts
text = "the quick brown fox jumps over the lazy dog the fox runs"
# Count word frequencies
counts = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
# Top 3 most common words
top = sorted(counts.items(), key=lambda kv: kv[1], reverse=True)[:3]
print("Top 3:")
for word, count in top:
print(f" {word}: {count}")
# Word lengths — dict comprehension from items
lengths = {word: len(word) for word in counts}
print(f"\nLongest word: {max(lengths, key=lengths.get)}")
# Group by first letter
by_letter = {}
for word in counts:
by_letter.setdefault(word[0], []).append(word)
print("\nBy letter:")
for letter, words in sorted(by_letter.items()):
print(f" {letter}: {words}")
# Output:
# Top 3:
# the: 3
# fox: 2
# quick: 1
#
# Longest word: quick
#
# By letter:
# b: ['brown']
# d: ['dog']
# f: ['fox']
# j: ['jumps']
# l: ['lazy']
# o: ['over']
# q: ['quick']
# r: ['runs']
# t: ['the']
Notice the three dict patterns combined. .get(key, 0) + 1 for counting. sorted(items, key=lambda kv: kv[1]) for ordering. .setdefault(key, []).append(x) for grouping. Those three lines are the entire toolkit for turning raw data into structured insight. Learn them and you can write a mini-analytics engine in 20 lines.
🏆 Challenge
Three tasks. Each uses a different dict pattern.
Task 1 — inventory counter
You have a list: items = ["apple", "banana", "apple", "cherry", "banana", "apple"]. Build a dict that counts how many of each item there are. Print the result.
Expected output: {'apple': 3, 'banana': 2, 'cherry': 1}. Use the .get(key, 0) + 1 pattern.
Task 2 — grade lookup
Create a dict called grades with at least three students and their scores. Then:
1. Print each name and score using .items().
2. Add a new student with grades["New Student"] = 88.
3. Use .get() to look up a student who doesn't exist — print "Not found" as the default.
4. Print the highest scorer using max(grades, key=grades.get).
Task 3 — nested user profile
Build a nested dict for a user with these fields: name, email, and an "address" dict containing street, city, and zip. Then:
1. Print the city using chained access.
2. Add a "country" key to the address.
3. Safely look up the user's phone number using chained .get() with "N/A" as the default.
4. Loop over all address fields and print them formatted.
⚠️ Common mistakes
The dict bugs that catch everyone.
user["phone"] when "phone" isn't there. Fix: user.get("phone") or user.get("phone", "N/A"). This is the single most common dict bug in Python — every backend engineer has shipped it at least once.
d[[1, 2]] = "x" → TypeError: unhashable type: 'list'. Fix: use a tuple d[(1, 2)]. Keys must be immutable.
for k in mydict: del mydict[k] raises RuntimeError: dictionary changed size during iteration. Fix: iterate over list(mydict) or mydict.copy().
user.get("address").get("city") crashes if "address" is missing — because .get() returns None, and None.get() fails. Fix: user.get("address", {}).get("city").
user.keys()[0] → TypeError: 'dict_keys' object is not subscriptable. Fix: list(user.keys())[0], or better, don't rely on positional access.
d["x"] = 5 assigns. d["x"] == 5 compares. Both are valid Python — the mistake is using one when you meant the other, which is silent since neither throws.
Python 3.6 and earlier: dict order was arbitrary. If you're reading old code or old tutorials, note this. In 3.7+ (your target), insertion order is guaranteed.
counts[w] += 1 crashes on first occurrence. Fix: counts[w] = counts.get(w, 0) + 1. This is the canonical dict idiom — you'll write it dozens of times.
➡️ What's next
You've covered the two essential data structures. On to files and errors.
❓ Frequently asked questions
What is a Python dictionary?
A dictionary is a mutable collection of key-value pairs. Each key maps to a value. Keys must be hashable (strings, numbers, tuples — not lists). Dictionaries are unordered in Python 3.6 and earlier, but preserve insertion order in 3.7+. Access values by key, not index: mydict['name'] rather than mydict[0]. They're the Python equivalent of a hash map or object in other languages.
How do I access a value in a Python dict?
Use square brackets: mydict['key']. This raises a KeyError if the key doesn't exist. To avoid errors, use .get('key') which returns None by default, or .get('key', default) to specify a fallback. For nested dicts, chain the access: mydict['user']['name']. Always use .get() when the key might not exist — it's the single biggest source of dict bugs.
What is the difference between dict.get() and dict['key']?
mydict['key'] raises KeyError if the key isn't there. mydict.get('key') returns None instead. mydict.get('key', default) returns default instead. Use .get() when the key might be missing and you have a sensible fallback. Use [] when the key must exist and a missing key is a real bug you want to catch.
How do I loop over a Python dictionary?
for key in mydict: gives you keys. for value in mydict.values(): gives values. for key, value in mydict.items(): gives both — this is the most common pattern. In Python 3.7+, iteration follows insertion order. To iterate in sorted order, use for key in sorted(mydict): or for k, v in sorted(mydict.items()):.
How do I add or update a key in a Python dict?
Assign to a key: mydict['new_key'] = value. If the key exists, the value is replaced. If not, the key is added. To update multiple keys at once, use .update() with another dict: mydict.update({'a': 1, 'b': 2}). To only add if missing, use .setdefault('key', default) — it returns the existing value or sets and returns the default.
How do I delete a key from a Python dict?
del mydict['key'] removes it — raises KeyError if missing. mydict.pop('key') removes and returns the value — raises KeyError if missing. mydict.pop('key', default) returns default instead of raising. To remove the last inserted item, use mydict.popitem() (Python 3.7+). To empty the dict, use mydict.clear(). Choose pop() when you need the removed value, del when you don't.
What is a dict comprehension in Python?
A one-line way to build a dict: {key_expr: value_expr for item in iterable if condition}. Example: {x: x*2 for x in range(4)} returns {0: 0, 1: 2, 2: 4, 3: 6}. Used for filtering, transforming, or inverting dicts. If you're building a dict in a for loop with .update() or [], a comprehension is almost always clearer.
Why do I get KeyError in Python?
Because you accessed a key that doesn't exist with mydict['missing_key']. Three common causes: (1) typo in the key name, (2) the key was never added, (3) the data structure isn't what you think it is. Fix: use .get('key') with a default, or check 'key' in mydict first. The KeyError message tells you the missing key — read it.