🌱 Beginner

Dictionaries

📖 Lesson 08 ⏱ 30 min 🧪 5 questions 💻 3 exercises

🎯 Learning Objectives

  • Create dictionaries and understand key-value structure
  • Access, add, update, and delete entries safely
  • Use essential methods: get(), keys(), values(), items(), setdefault(), update()
  • Iterate over dictionaries in multiple ways
  • Build nested dictionaries and work with real-world data shapes
  • Use dictionary comprehensions for concise dict creation

What is a Dictionary?

A dictionary (dict) is an unordered* collection of key-value pairs. Think of it like a real dictionary: you look up a word (key) to find its definition (value).

*As of Python 3.7+, dicts maintain insertion order, but they're not indexed by position.

# Creating dictionaries
person = {
    "name": "Alice",
    "age": 30,
    "city": "London"
}

# Other ways to create
empty = {}
from_constructor = dict(name="Bob", age=25)
from_pairs = dict([("x", 1), ("y", 2)])

# Keys must be hashable (immutable): str, int, float, tuple, bool
# Values can be anything
config = {
    "debug": True,
    "max_retries": 3,
    "endpoints": ["/api/users", "/api/posts"],
    (0, 0): "origin"   # tuple as key — valid!
}
create_dicts.py
Dictionaries are Python's implementation of a hash map. Key lookups are O(1) — constant time regardless of dictionary size. This makes them extremely fast for data that you look up by a specific identifier.

Accessing Values

person = {"name": "Alice", "age": 30, "city": "London"}

# Bracket notation — raises KeyError if key doesn't exist
print(person["name"])    # "Alice"
# print(person["email"])  → KeyError: 'email'

# .get() — returns None (or a default) if key is missing
print(person.get("email"))           # None
print(person.get("email", "N/A"))   # "N/A"
print(person.get("name", "N/A"))    # "Alice" (key exists)

# Check if a key exists
print("name" in person)     # True
print("email" in person)    # False
print("Alice" in person)    # False — 'in' checks KEYS, not values
accessing.py
Use .get(key, default) when a key might be missing and you want a fallback. Use dict[key] when the key must exist — a KeyError is a useful signal that something is wrong.

Adding & Updating

person = {"name": "Alice", "age": 30}

# Add a new key-value pair
person["email"] = "alice@example.com"

# Update an existing value
person["age"] = 31

# update() — merge another dict (overwrites existing keys)
person.update({"city": "Paris", "age": 32})
print(person)
# {'name': 'Alice', 'age': 32, 'email': 'alice@example.com', 'city': 'Paris'}

# setdefault() — set only if key doesn't exist
person.setdefault("country", "France")   # adds "country": "France"
person.setdefault("name", "Unknown")     # does nothing — "name" already exists
print(person["name"])  # "Alice"

# Merge with | operator (Python 3.9+)
defaults = {"theme": "dark", "lang": "en"}
overrides = {"lang": "fr", "font_size": 14}
merged = defaults | overrides
print(merged)  # {'theme': 'dark', 'lang': 'fr', 'font_size': 14}
modifying.py

Removing Entries

MethodActionReturns
del dict[key]Remove key (KeyError if missing)
.pop(key)Remove & return value (KeyError if missing)The value
.pop(key, default)Remove & return value (default if missing)Value or default
.popitem()Remove & return last inserted pair(key, value) tuple
.clear()Remove all entriesNone
d = {"a": 1, "b": 2, "c": 3, "d": 4}

# del
del d["a"]          # d = {"b": 2, "c": 3, "d": 4}

# pop with default (safe)
val = d.pop("z", 0)   # 0 (key didn't exist, no error)
val = d.pop("b")       # 2, d = {"c": 3, "d": 4}

# popitem (LIFO in 3.7+)
last = d.popitem()     # ("d", 4), d = {"c": 3}
removing.py

Key Methods Reference

MethodReturns
.keys()View of all keys
.values()View of all values
.items()View of all (key, value) pairs
.get(key, default)Value for key, or default
.setdefault(key, default)Value for key; sets it if missing
.update(other)None (merges other into dict)
.copy()Shallow copy of the dict
len(d)Number of key-value pairs
person = {"name": "Alice", "age": 30, "city": "London"}

print(list(person.keys()))    # ['name', 'age', 'city']
print(list(person.values()))  # ['Alice', 30, 'London']
print(list(person.items()))   # [('name', 'Alice'), ('age', 30), ('city', 'London')]

print(len(person))  # 3
methods.py

Iterating Over Dictionaries

scores = {"Alice": 85, "Bob": 92, "Charlie": 78}

# Iterate over keys (default)
for name in scores:
    print(name)

# Iterate over values
for score in scores.values():
    print(score)

# Iterate over key-value pairs (most common)
for name, score in scores.items():
    print(f"{name}: {score}")

# Sorted iteration
for name in sorted(scores):
    print(f"{name}: {scores[name]}")

# Sort by value
for name, score in sorted(scores.items(), key=lambda x: x[1], reverse=True):
    print(f"{name}: {score}")
# Bob: 92, Alice: 85, Charlie: 78
iteration.py
Always use .items() when you need both key and value. Iterating with for k in dict: and then accessing dict[k] works but is less readable and slightly slower.

Dictionary Comprehensions

Like list comprehensions, but produce a dictionary:

# Basic: {key_expr: value_expr for item in iterable}
squares = {n: n ** 2 for n in range(1, 6)}
print(squares)  # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# With condition
even_squares = {n: n ** 2 for n in range(1, 11) if n % 2 == 0}
print(even_squares)  # {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}

# Transform existing dict
prices = {"apple": 1.20, "banana": 0.50, "cherry": 2.00}
discounted = {item: round(price * 0.9, 2) for item, price in prices.items()}
print(discounted)  # {'apple': 1.08, 'banana': 0.45, 'cherry': 1.8}

# Swap keys and values
flipped = {v: k for k, v in prices.items()}
print(flipped)  # {1.2: 'apple', 0.5: 'banana', 2.0: 'cherry'}

# From two lists
keys = ["name", "age", "city"]
values = ["Alice", 30, "London"]
person = dict(zip(keys, values))
print(person)  # {'name': 'Alice', 'age': 30, 'city': 'London'}
dict_comp.py

Nested Dictionaries

Dictionaries often contain other dictionaries — this is how structured data (like JSON) is represented in Python:

users = {
    "alice": {
        "email": "alice@example.com",
        "age": 30,
        "roles": ["admin", "editor"]
    },
    "bob": {
        "email": "bob@example.com",
        "age": 25,
        "roles": ["viewer"]
    }
}

# Access nested values
print(users["alice"]["email"])        # alice@example.com
print(users["bob"]["roles"][0])       # viewer

# Safe nested access with .get()
phone = users["alice"].get("phone", "not provided")
print(phone)  # "not provided"

# Add to nested structure
users["alice"]["phone"] = "+44 7700 123456"

# Iterate nested
for username, profile in users.items():
    print(f"{username}: {profile['email']} ({', '.join(profile['roles'])})")
nested.py
For deeply nested access that might fail at any level, consider a helper function or third-party libraries like glom. In production code you'll often validate structure with Pydantic (Lesson 38).

Common Dict Patterns

# Counting occurrences
text = "the cat sat on the mat"
word_count = {}
for word in text.split():
    word_count[word] = word_count.get(word, 0) + 1
print(word_count)  # {'the': 2, 'cat': 1, 'sat': 1, 'on': 1, 'mat': 1}

# Better: use collections.Counter
from collections import Counter
word_count = Counter(text.split())
print(word_count.most_common(2))  # [('the', 2), ('cat', 1)]

# Grouping
students = [("Alice", "A"), ("Bob", "B"), ("Charlie", "A"), ("Dave", "B")]
groups = {}
for name, grade in students:
    groups.setdefault(grade, []).append(name)
print(groups)  # {'A': ['Alice', 'Charlie'], 'B': ['Bob', 'Dave']}

# Better: use collections.defaultdict
from collections import defaultdict
groups = defaultdict(list)
for name, grade in students:
    groups[grade].append(name)

# Merging multiple dicts (Python 3.9+)
a = {"x": 1}
b = {"y": 2}
c = {"z": 3}
merged = a | b | c  # {'x': 1, 'y': 2, 'z': 3}
patterns.py
🤖

Ask your AI tutor! Not sure when to use .get() vs bracket access? Want to know how defaultdict works internally? Dictionaries are everywhere in Python — solid understanding here pays off hugely.

💻 Exercises

01 Word Counter

Write a script that takes the string "to be or not to be that is the question" and builds a dictionary mapping each word to how many times it appears. Print the result sorted by count (highest first).

Show solution
text = "to be or not to be that is the question"
counts = {}
for word in text.split():
    counts[word] = counts.get(word, 0) + 1

# Sort by count descending
for word, n in sorted(counts.items(), key=lambda x: x[1], reverse=True):
    print(f"{word}: {n}")
# to: 2
# be: 2
# or: 1  ... etc.
02 Phonebook

Create a phonebook dict with at least 5 entries (name → phone number). Write code that: looks up a name (handling the case where it's not found), adds a new entry, deletes an entry, and prints all contacts alphabetically.

Show solution
phonebook = {
    "Alice": "555-0101",
    "Bob": "555-0102",
    "Charlie": "555-0103",
    "Dave": "555-0104",
    "Eve": "555-0105"
}

# Look up (safe)
name = "Frank"
number = phonebook.get(name, "Not found")
print(f"{name}: {number}")  # Frank: Not found

# Add
phonebook["Frank"] = "555-0106"

# Delete
del phonebook["Dave"]

# Print alphabetically
for name in sorted(phonebook):
    print(f"  {name}: {phonebook[name]}")
03 Invert a Dictionary

Given {"a": 1, "b": 2, "c": 3}, create a new dictionary where the keys and values are swapped: {1: "a", 2: "b", 3: "c"}. Use a dictionary comprehension. Then think: what happens if two keys have the same value?

Show solution
original = {"a": 1, "b": 2, "c": 3}

# Simple inversion (assumes unique values)
inverted = {v: k for k, v in original.items()}
print(inverted)  # {1: 'a', 2: 'b', 3: 'c'}

# If values aren't unique, collect keys in a list
data = {"a": 1, "b": 2, "c": 1}
inverted_safe = {}
for k, v in data.items():
    inverted_safe.setdefault(v, []).append(k)
print(inverted_safe)  # {1: ['a', 'c'], 2: ['b']}