Dictionaries: Intermediate

Spotify's Discover Weekly algorithm uses Counter dictionaries to tally genre and artist frequencies across each user's listening history, transforming raw play events into a taste fingerprint that drives recommendations for 600 million listeners every week. A Counter turns a list of a thousand song plays into a ranked frequency map in a single line, replacing what would otherwise be a dozen lines of manual tallying. The intermediate dictionary patterns in this lesson, including Counter, defaultdict, and dict comprehensions, are the tools behind that kind of data transformation.

Iterating Over Dictionaries

Daily Life
Interviews

Loop through keys, values, or both

Iterating means looping through each item in a collection. With lists, you iterate over elements by position. With dictionaries, you have three choices: iterate over keys, values, or both. Let's explore each approach.

Iterating Over Keys

When you loop directly over a dictionary, you iterate over its keys. This is the default behavior and the most common pattern:
1user = {"name": "Alice", "age": 28, "city": "Seattle"}
2
3# Looping directly gives you keys
4for key in user:
5 print(key)
>>>Output
name
age
city

You can also explicitly use the .keys() method, which does exactly the same thing but makes your intent clearer:

1user = {"name": "Alice", "age": 28, "city": "Seattle"}
2
3for key in user.keys():
4 print(key)
>>>Output
name
age
city
TIP
Some developers prefer explicit .keys() for readability. Others prefer the shorter version. Both are correct. At major tech companies, you'll see both styles depending on team conventions.

Iterating Over Values

When you only need the values and don't care about the keys, use .values(). This is useful for calculations or aggregations:

1prices = {"apple": 1.50, "banana": 0.75, "orange": 2.00}
2
3# Loop through just the values
4total = 0
5for price in prices.values():
6 total = total + price
7
8print("Total:", total)
>>>Output
Total: 4.25

Iterating Keys and Values

Often you need both the key and the value together. The .items() method returns pairs, which you can unpack directly in the for loop. This is one of the most useful dictionary patterns:

1user = {"name": "Alice", "age": 28, "city": "Seattle"}
2
3# .items() gives you (key, value) pairs
4for key, value in user.items():
5 print(key + ":", value)
>>>Output
name: Alice
age: 28
city: Seattle

The syntax for key, value in dict.items() is called tuple unpacking. Each item from .items() is a pair (technically a tuple), and Python automatically splits it into two variables. You can name these variables anything, but key/value or k/v are conventional.

.keys().values().items()
.keys()
Keys only
Loop through key names
.values()
Values only
Loop through stored data
.items()
Both together
Key-value pairs at once
Fill in the blanks to complete two tasks: sum up all the prices, then get a list of all item names.
Python Quiz

> You need the numeric prices to calculate a total, and the item names to display a list. Choose the right iteration method for each task.

prices = {
    "apple": 1.50,
    "banana": 0.75,
    "orange": 2.00
}
total = 0
for price in prices.___():
    total = total + price
names = list(prices.___())
print("Total:", total)
print("Items:", names)
values
keys
keys
items
items
values

Choosing the right iteration method is about being explicit in your intent. .keys() signals "I only care about names," .values() signals "I only care about the data," and .items() signals "I need both together."

The .items() method is the most versatile and is what you will reach for most often in data transformation code. It lets you reference both the key and value by readable names rather than indexing into a tuple.

Since Python 3.7, dictionary order is guaranteed to be insertion order. This means looping over keys, values, or items always produces them in the order they were added, making dictionary iteration predictable.

Dictionary View Objects

Daily Life
Interviews

Use views, update, and setdefault

When you call .keys(), .values(), or .items(), Python doesn't create a list. Instead, it returns a special "view" object. Views are lightweight and always reflect the current state of the dictionary.

1user = {"name": "Alice", "age": 28}
2
3keys = user.keys()
4print("Keys:", keys)
5
6# Views update when dict changes
7user["city"] = "Seattle"
8print("Keys after adding:", keys)
>>>Output
Keys: dict_keys(['name', 'age'])
Keys after adding: dict_keys(['name', 'age', 'city'])

Notice that we didn't call .keys() again, but the view automatically includes the new "city" key. This is different from lists, where you'd need to recreate the list to see changes.

Converting Views to Lists

Sometimes you need an actual list of keys or values (for example, to sort them or access by index). Use the list() function to convert a view:

1user = {"name": "Alice", "age": 28, "city": "Seattle"}
2
3# Convert view to list
4key_list = list(user.keys())
5value_list = list(user.values())
6
7print("Keys as list:", key_list)
8print("First key:", key_list[0])
>>>Output
Keys as list: ['name', 'age', 'city']
First key: name
View Objects
  • Lightweight (low memory)
  • Updates with dictionary
  • Good for iteration
  • Cannot index directly
Converted Lists
  • Uses more memory
  • Static snapshot
  • Can be indexed: list[0]
  • Can be sorted/modified

The .update() Method

The .update() method adds or updates multiple key-value pairs at once. You can pass it another dictionary, and all its entries will be merged into the original:

1user = {"name": "Alice", "age": 28}
2print("Before:", user)
3
4# Add multiple entries at once
5user.update({"city": "Seattle", "country": "USA"})
6print("After update:", user)
>>>Output
Before: {'name': 'Alice', 'age': 28}
After update: {'name': 'Alice', 'age': 28, 'city': 'Seattle', 'country': 'USA'}

If a key already exists, .update() overwrites it with the new value. This makes it useful for applying changes or defaults:

1user = {"name": "Alice", "age": 28}
2
3# Update existing key + add new key
4user.update({"age": 29, "email": "alice@email.com"})
5print(user)
>>>Output
{'name': 'Alice', 'age': 29, 'email': 'alice@email.com'}
TIP
Think of .update() as "merge this dictionary into mine." It's similar to adding items one by one with dict[key] = value, but more efficient for multiple items.

The .setdefault() Method

The .setdefault() method is a combination of "check if key exists" and "add it if it doesn't." It returns the value for a key, but if the key doesn't exist, it first sets it to a default value:

1user = {"name": "Alice"}
2
3# Key exists: returns existing value
4name = user.setdefault("name", "Unknown")
5print("Name:", name)
6print("Dict:", user)
>>>Output
Name: Alice
Dict: {'name': 'Alice'}
1user = {"name": "Alice"}
2
3# Key missing: sets and returns default
4city = user.setdefault("city", "Unknown")
5print("City:", city)
6print("Dict:", user)
>>>Output
City: Unknown
Dict: {'name': 'Alice', 'city': 'Unknown'}

This is particularly useful when building up dictionary data incrementally. A common pattern is using .setdefault() to initialize lists or counters:

1# Group items by their first letter
2words = ["apple", "banana", "apricot", "blueberry", "avocado"]
3groups = {}
4
5for word in words:
6 first_letter = word[0]
7 # If key doesn't exist, create empty list, then append
8 groups.setdefault(first_letter, []).append(word)
9
10print(groups)
>>>Output
{'a': ['apple', 'apricot', 'avocado'], 'b': ['banana', 'blueberry']}
.get(key, default)
  • Returns value or default
  • Never modifies the dict
  • Read-only operation
  • Use for safe lookups
.setdefault(key, val)
  • Returns value or default
  • Adds key if missing
  • Modifies the dict
  • Use to initialize entries

Merging Dictionaries

Daily Life
Interviews

Combine dicts with overrides intact

Combining two dictionaries into one is a common operation. Python provides several ways to do this, each with different trade-offs.

Method 1: Using .update()

As we just saw, .update() merges a dictionary into an existing one. This modifies the original dictionary:

1defaults = {"theme": "light", "volume": 50}
2user_prefs = {"volume": 80, "language": "en"}
3
4# Merge user_prefs INTO defaults
5defaults.update(user_prefs)
6print(defaults)
>>>Output
{'theme': 'light', 'volume': 80, 'language': 'en'}

Method 2: | Operator (3.9+)

Modern Python (3.9 and later) introduced the | operator for merging dictionaries. This creates a NEW dictionary without modifying the originals:
1defaults = {"theme": "light", "volume": 50}
2user_prefs = {"volume": 80, "language": "en"}
3
4# Create a new merged dictionary
5merged = defaults | user_prefs
6print("Merged:", merged)
7print("Original defaults:", defaults)
>>>Output
Merged: {'theme': 'light', 'volume': 80, 'language': 'en'}
Original defaults: {'theme': 'light', 'volume': 50}
When both dictionaries have the same key, the value from the right-side dictionary (after the |) wins. This is intuitive: you're "applying" user preferences on top of defaults.

Method 3: |= Operator

The |= operator is like .update() but uses the new merge syntax. It modifies the left dictionary in place:

1defaults = {"theme": "light", "volume": 50}
2user_prefs = {"volume": 80, "language": "en"}
3
4defaults |= user_prefs
5print(defaults)
>>>Output
{'theme': 'light', 'volume': 80, 'language': 'en'}
dict1 | dict2
  • Creates new dictionary
  • Originals unchanged
  • Returns the merged result
  • Pure functional style
dict1 |= dict2
  • Modifies dict1 in place
  • dict1 gains new items
  • Returns None
  • Same as .update()
Fill in the blanks to merge user preferences on top of defaults without modifying the original defaults dictionary.
Python Quiz

> You want to apply user preferences on top of defaults, but keep the original defaults dictionary unchanged for other users. First take a snapshot, then merge.

defaults = {
    "theme": "light",
    "volume": 50
}
user_prefs = {
    "volume": 80, "lang": "en"
}
combined = defaults.___()
combined.___(user_prefs)
print(combined)
items
update
clear
update
copy
copy
The defaults-plus-overrides pattern is ubiquitous in Python applications. Configuration systems, API clients, and testing frameworks all use it. A base dictionary provides sensible defaults; individual callers supply only the values they want to customize.
The | operator introduced in Python 3.9 is the cleanest syntax for non-destructive merging. It communicates intent directly: "create a new dictionary that is the combination of these two." Use it in modern codebases when you can.

Merge order matters when both dictionaries have the same key. The rightmost value always wins. Design your merge calls so that the higher-priority dictionary is on the right side of | or passed as the argument to .update().

Nested Dictionaries

Daily Life
Interviews

Navigate multi-level data safely

Dictionary values can be any Python type, including other dictionaries. This allows you to represent complex, hierarchical data. Real-world data is almost always nested.
1company = {
2 "name": "TechCorp",
3 "address": {
4 "street": "123 Main St",
5 "city": "Seattle",
6 "zip": "98101"
7 },
8 "employees": 150
9}
10
11# Access nested values
12print(company["address"]["city"])
>>>Output
Seattle
Each bracket access goes one level deeper. The expression company["address"] returns the nested dictionary, and then ["city"] accesses a key within that nested dictionary.

Modifying Nested Values

You can modify nested values using the same chained bracket syntax:
1company = {
2 "name": "TechCorp",
3 "address": {
4 "city": "Seattle",
5 "zip": "98101"
6 }
7}
8
9# Modify nested value
10company["address"]["city"] = "Portland"
11print(company["address"])
12
13# Add new key to nested dict
14company["address"]["state"] = "OR"
15print(company["address"])
>>>Output
{'city': 'Portland', 'zip': '98101'}
{'city': 'Portland', 'zip': '98101', 'state': 'OR'}

Safe Nested Access

Accessing deeply nested data can fail if any intermediate key is missing. The safe approach is to check each level or use .get() with defaults:

1data = {"user": {"profile": {"name": "Alice"}}}
2
3# Safe access with .get() chaining
4name = data.get("user", {}).get("profile", {}).get("name", "Unknown")
5print("Name:", name)
6
7# Missing intermediate key returns default
8missing = data.get("user", {}).get("settings", {}).get("theme", "light")
9print("Theme:", missing)
>>>Output
Name: Alice
Theme: light
TIP
The pattern .get("key", {}) returns an empty dictionary if the key is missing. This lets you continue chaining .get() calls without errors. It's a common idiom for safely navigating nested data.
Following a few simple rules prevents most nested dictionary errors.
Nested Dictionary Safety Rules
  • Always use .get() when a key might be missing
  • Chain .get() calls with {} default for nested access
  • Check intermediate levels before deeply nested writes
  • Consider try/except for complex nested structures
  • Document the expected shape of nested data
Practice navigating nested data by choosing the correct access pattern for a multi-level dictionary.
Fill in the Blank

> A user dictionary has a "name" key inside "user" but no "age" key. Pick the access pattern that returns a safe default instead of crashing.

data = {"user": {"name": "Alice"}}
result = 
print(result)
Nested dictionaries are the natural representation of JSON data, which you will work with constantly when calling REST APIs. Every JSON object becomes a Python dict, and every JSON array becomes a Python list.
Deep nesting is a code smell in your own data structures. If you find yourself chaining more than two or three levels of brackets, consider flattening the structure or creating a helper function to extract the value safely.

The try/except approach is an alternative to chained .get() for complex nested access. Wrap the access in a try block catching KeyError, which lets you write the direct bracket path and handle all missing-key cases in one except clause.

Dictionaries with Lists

Daily Life
Interviews

Group multiple items under one key

A very common pattern is using lists as dictionary values. This lets you group multiple items under a single key, such as all orders for a customer or all tags for an article:
1user = {
2 "name": "Alice",
3 "emails": ["alice@work.com", "alice@personal.com"],
4 "roles": ["admin", "editor"]
5}
6
7# Access the list
8print(user["emails"])
9
10# Access an item within the list
11print(user["emails"][0])
12
13# Append to the nested list
14user["emails"].append("alice@backup.com")
15print(user["emails"])
>>>Output
['alice@work.com', 'alice@personal.com']
alice@work.com
['alice@work.com', 'alice@personal.com', 'alice@backup.com']
This pattern appears constantly in data engineering. API responses, configuration files, database records with multi-valued fields all use dictionaries with lists.

Building Lists Over Time

A frequent task is grouping items into lists based on some criterion. The .setdefault() method we learned earlier is perfect for this:

1# Group students by their grade
2students = [
3 {"name": "Alice", "grade": "A"},
4 {"name": "Bob", "grade": "B"},
5 {"name": "Carol", "grade": "A"},
6 {"name": "Dave", "grade": "B"}
7]
8
9by_grade = {}
10for student in students:
11 grade = student["grade"]
12 by_grade.setdefault(grade, []).append(student["name"])
13
14print(by_grade)
>>>Output
{'A': ['Alice', 'Carol'], 'B': ['Bob', 'Dave']}
Lists as values
Lists as values
Group multiple items under one key, like tags or roles for a user.
Dicts as values
Dicts as values
Nest dictionaries for hierarchical data like addresses or profiles.
.setdefault() grouping
.setdefault() grouping
Initialize a list on first access, then append items incrementally.

Copying Dictionaries

Daily Life
Interviews

Clone dicts without shared mutations

When you assign a dictionary to a new variable, you don't create a copy. Both variables point to the same dictionary. This is critical to understand:
1original = {"a": 1, "b": 2}
2# NOT a copy - both point to the same dict
3reference = original
4
5reference["c"] = 3
6print("Original:", original)
7print("Reference:", reference)
>>>Output
Original: {'a': 1, 'b': 2, 'c': 3}
Reference: {'a': 1, 'b': 2, 'c': 3}
Do
  • Use .copy() when you need independent data
  • Pass copies to functions that may modify them
  • Be explicit about whether you want shared state
Don't
  • Assume "new_dict = old_dict" creates a copy
  • Modify a dictionary inside a function unknowingly
  • Forget that both variables point to the same object

Shallow Copy with .copy()

The .copy() method creates a new dictionary with the same key-value pairs. Changes to the copy don't affect the original:

1original = {"a": 1, "b": 2}
2copy = original.copy()
3
4copy["c"] = 3
5print("Original:", original)
6print("Copy:", copy)
>>>Output
Original: {'a': 1, 'b': 2}
Copy: {'a': 1, 'b': 2, 'c': 3}

However, .copy() only creates a shallow copy. If your dictionary contains mutable values (like lists or nested dictionaries), those inner objects are NOT copied:

1original = {"items": [1, 2, 3]}
2shallow = original.copy()
3
4shallow["items"].append(4)
5print("Original:", original)
6print("Shallow:", shallow)
>>>Output
Original: {'items': [1, 2, 3, 4]}
Shallow: {'items': [1, 2, 3, 4]}
TIP
For dictionaries with nested mutable data, you need a deep copy. We'll cover that in the advanced lesson. For flat dictionaries, .copy() is sufficient.
Fill in the blanks to take a snapshot of a dictionary, then modify the original. Think about what each operation returns.
Python Quiz

> You want to save a snapshot of the data before adding a new entry. After the snapshot, modifications to the original should not affect it.

data = {"x": 10, "y": 20}
snapshot = data.___()
data.___({"z": 30})
print(len(data))
print(len(snapshot))
update
copy
clear
pop
items
keys
Understanding reference semantics vs copy semantics is crucial for avoiding bugs when passing dictionaries to functions. Functions receive a reference by default, so any mutation inside the function is visible to the caller.
Shallow copy is sufficient for flat dictionaries - those where values are simple types like strings and numbers. Deep copy is only needed when values themselves are mutable containers like lists or nested dicts.
The pattern of copying before passing to a function that might mutate it is called defensive copying. It protects your data at the cost of extra memory, which is usually worthwhile for correctness and debugging clarity.

Dictionary Comprehensions

Daily Life
Interviews

Build filtered dicts in one line

Just like list comprehensions create lists in a single expression, dictionary comprehensions create dictionaries. The syntax uses curly braces with a key:value expression:
1# Create a dictionary of number -> square
2squares = {n: n * n for n in range(1, 6)}
3print(squares)
>>>Output
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

The pattern is {key_expression: value_expression for item in iterable}. This is a concise way to transform data into dictionary form:

1names = ["Alice", "Bob", "Charlie"]
2name_lengths = {name: len(name) for name in names}
3print(name_lengths)
>>>Output
{'Alice': 5, 'Bob': 3, 'Charlie': 7}

Filter with Comprehensions

You can add an if clause to filter which items are included:
1scores = {"Alice": 85, "Bob": 92, "Carol": 78, "Dave": 95}
2
3# Keep only scores >= 90
4high_scores = {name: score for name, score in scores.items() if score >= 90}
5print(high_scores)
>>>Output
{'Bob': 92, 'Dave': 95}

Transforming Keys or Values

Comprehensions are powerful for transforming dictionary data:
1prices = {"apple": 1.50, "banana": 0.75, "orange": 2.00}
2
3# Apply 10% discount to all prices
4discounted = {item: price * 0.9 for item, price in prices.items()}
5print(discounted)
6
7# Convert keys to uppercase
8upper = {k.upper(): v for k, v in prices.items()}
9print(upper)
>>>Output
{'apple': 1.35, 'banana': 0.675, 'orange': 1.8}
{'APPLE': 1.5, 'BANANA': 0.75, 'ORANGE': 2.0}
Try building a dictionary comprehension yourself. Choose the right expression to transform a list of names into a dictionary of name-length pairs.
Fill in the Blank

> You have a list of words ["hello", "world", "python"] and want to build a dictionary mapping each word to its character count. Pick the comprehension that does this correctly.

words = ["hello", "world", "python"]
result = 
print(result)
Dictionary comprehensions are particularly powerful for transforming one dictionary into another. Filtering keys, normalizing values, and inverting key-value pairs are all one-liners with comprehension syntax.
The if clause in a comprehension runs before the key-value expression. This means you can safely use the item in the condition knowing that filtered-out items will never reach the key-value pair evaluation.
For very complex transformations, a regular for loop is often clearer than a dense comprehension. The goal is readable code - comprehensions are a tool for clarity, not for showing off how much you can fit in one line.

Common Dictionary Patterns

Daily Life
Interviews

Invert, count, and group with dicts

These patterns appear constantly in real code. Once you recognize them, you will reach for them instinctively when solving similar problems.
01
Invert
Swap keys and values for reverse lookups
02
Count
Track frequency of items with .get(key, 0) + 1
03
Group
Collect related items into lists under shared keys

Inverting a Dictionary

Swapping keys and values is useful when you need reverse lookups:
1country_codes = {"USA": 1, "UK": 44, "Japan": 81}
2
3# Invert: value becomes key, key becomes value
4code_to_country = {{code: country for country, code in country_codes.items()}}
5print(code_to_country)
>>>Output
{1: 'USA', 44: 'UK', 81: 'Japan'}
TIP
Inverting only works cleanly if all values are unique. If multiple keys share the same value, some data will be lost during inversion.

Counting with Dictionaries

We saw a counting example in the beginner lesson. Here's a cleaner version using .get():
1text = "mississippi"
2letter_counts = {}
3
4for letter in text:
5 letter_counts[letter] = letter_counts.get(letter, 0) + 1
6
7print(letter_counts)
>>>Output
{'m': 1, 'i': 4, 's': 4, 'p': 2}

The expression dict.get(key, 0) + 1 returns the current count (or 0 if missing) and adds 1. This is a classic Python idiom for counting.

This counting loop has a bug. The code tries to count letters but crashes on the first iteration. Remove the tile causing the error to fix it.
Debug Challenge

> This counting loop accesses counts[ch] directly on the first encounter, but the key does not exist yet. It needs a safe default value.

KeyError: 'h' - the key doesn't exist yet on first access

Mastering dictionary patterns is essential for building efficient, readable data pipelines. Put these techniques to the test with hands-on challenges in the Python Builder.

The grouping pattern collects items into lists under a shared key. Use setdefault or a defaultdict to accumulate values without checking for key existence first.

Combining invert, count, and group gives you a powerful toolkit for transforming raw data into structured summaries with only a few lines of Python.
PUTTING IT ALL TOGETHER

> You are a data engineer at Amplitude aggregating raw clickstream events into a nested session summary dictionary, iterating over every session's key-value pairs, merging config overrides, and using a dict comprehension to build a filtered view of high-value sessions for the weekly retention report.

Iterating with .items() processes every session ID paired with its metrics dict to apply aggregation logic across the full event log.
Nested dict structures store each session's event counts, durations, and page sequences as inner dicts keyed by event type.
.update() merges a config-override dictionary into the base session summary without overwriting unrelated keys.
dict comprehension builds the filtered high-value session view in a single expression by combining key selection and value transformation.
KEY TAKEAWAYS
Iterate with for key in dict, .keys(), .values(), or .items()
.items() gives (key, value) pairs - the most versatile option
.update() merges another dictionary into yours
.setdefault() returns a value, creating the key first if needed
Use | to merge dicts into a new dict (Python 3.9+)
Nested dicts use chained brackets: dict["a"]["b"]
.copy() makes a shallow copy; nested objects are shared
Dict comprehensions: {k: v for k, v in iterable}

Navigate nested data with confidence

Category
Python
Difficulty
intermediate
Duration
37 minutes
Challenges
3 hands-on challenges

Topics covered: Iterating Over Dictionaries, Dictionary View Objects, Merging Dictionaries, Nested Dictionaries, Dictionaries with Lists, Copying Dictionaries, Dictionary Comprehensions, Common Dictionary Patterns

Lesson Sections

  1. Iterating Over Dictionaries

    Iterating means looping through each item in a collection. With lists, you iterate over elements by position. With dictionaries, you have three choices: iterate over keys, values, or both. Let's explore each approach. Iterating Over Keys When you loop directly over a dictionary, you iterate over its keys. This is the default behavior and the most common pattern: Iterating Over Values Iterating Keys and Values Fill in the blanks to complete two tasks: sum up all the prices, then get a list of all

  2. Dictionary View Objects

    Converting Views to Lists The .update() Method The .setdefault() Method

  3. Merging Dictionaries (concepts: pyDictMerge)

    Combining two dictionaries into one is a common operation. Python provides several ways to do this, each with different trade-offs. Method 1: Using .update() Method 2: | Operator (3.9+) Modern Python (3.9 and later) introduced the | operator for merging dictionaries. This creates a NEW dictionary without modifying the originals: When both dictionaries have the same key, the value from the right-side dictionary (after the |) wins. This is intuitive: you're "applying" user preferences on top of de

  4. Nested Dictionaries (concepts: pyDictNested)

    Dictionary values can be any Python type, including other dictionaries. This allows you to represent complex, hierarchical data. Real-world data is almost always nested. Each bracket access goes one level deeper. The expression company["address"] returns the nested dictionary, and then ["city"] accesses a key within that nested dictionary. Modifying Nested Values You can modify nested values using the same chained bracket syntax: Safe Nested Access Following a few simple rules prevents most nest

  5. Dictionaries with Lists

    A very common pattern is using lists as dictionary values. This lets you group multiple items under a single key, such as all orders for a customer or all tags for an article: This pattern appears constantly in data engineering. API responses, configuration files, database records with multi-valued fields all use dictionaries with lists. Building Lists Over Time

  6. Copying Dictionaries

    When you assign a dictionary to a new variable, you don't create a copy. Both variables point to the same dictionary. This is critical to understand: Shallow Copy with .copy() Fill in the blanks to take a snapshot of a dictionary, then modify the original. Think about what each operation returns. Understanding reference semantics vs copy semantics is crucial for avoiding bugs when passing dictionaries to functions. Functions receive a reference by default, so any mutation inside the function is

  7. Dictionary Comprehensions (concepts: pyDictComprehension)

    Just like list comprehensions create lists in a single expression, dictionary comprehensions create dictionaries. The syntax uses curly braces with a key:value expression: Filter with Comprehensions You can add an if clause to filter which items are included: Transforming Keys or Values Comprehensions are powerful for transforming dictionary data: Try building a dictionary comprehension yourself. Choose the right expression to transform a list of names into a dictionary of name-length pairs. Dic

  8. Common Dictionary Patterns

    These patterns appear constantly in real code. Once you recognize them, you will reach for them instinctively when solving similar problems. Inverting a Dictionary Swapping keys and values is useful when you need reverse lookups: Counting with Dictionaries We saw a counting example in the beginner lesson. Here's a cleaner version using .get(): This counting loop has a bug. The code tries to count letters but crashes on the first iteration. Remove the tile causing the error to fix it. Mastering d