Skip to contentSkip to navigation
Module 04 • Lesson 03PEP 584 Dictionary Union (| & |=)Production Methods & Deep Merging

Dictionary Methods & Key Merging

Master modern PEP 584 union operators (| and |=), single-pass accumulation with .setdefault(), shallow vs deep cloning pitfalls, view-based schema diffing, and recursive multi-tier configuration engines.

Est. Duration

15 mins

Architectural deep dive

Merge Paradigm

PEP 584

Right-hand operand precedence

Accumulator

setdefault()

Zero double-lookup overhead

Challenges

17 Checks

Interactive mastery

What You Will Master

Apply modern PEP 584 dictionary merge (|) and update (|=) operators, understanding conflict resolution and operand precedence.
Utilize .setdefault() and defaultdict paradigms for accumulator and grouping patterns without double-lookup performance penalties.
Extract and prune key-value pairs cleanly using .pop(key, default), .popitem(), and .clear() with proper exception handling.
Differentiate shallow copying (dict.copy()) from recursive deep copying (copy.deepcopy()) in nested configuration trees.
Perform set algebra operations on dictionary view objects (d.items(), d.keys()) to compute schema diffs and modifications.
Implement recursive deep-merging algorithms to combine multi-tier JSON configuration environments without destructive overwrites.

01 / Deep Architectural Foundations

In production Python microservices, distributed architectures, and data engineering pipelines, dictionaries are rarely static. Services constantly merge multi-tiered configuration layers (defaults, environment flags, dynamic cluster overrides), compute delta diffs between database models, and group high-throughput streaming events.

In Python 3.9+, PEP 584 introduced dedicated dictionary merge operators (| and |=), replacing legacy unpacking tricks like {**d1, **d2}. These operators are not merely cosmetic: they retain subclass types, provide deterministic right-hand precedence, and make dictionary algebra as natural as numeric arithmetic.

The Merge Operator Invariant: Rightmost Operand Wins

In any merge expression merged = base | overrides, if a key exists in both dictionaries, the right-hand value strictly takes precedence (merged[k] = overrides[k]). Neither base noroverrides is mutated.

Method Mechanics & Guarantees

  • Binary Merge (|): Allocates a brand new dictionary. Pure function with zero mutation side effects.
  • Augmented Update (|=): Mutates the left-hand dictionary in place, reusing its internal hash buckets.
  • Single-Pass setdefault(): Searches and lazily inserts default values without redundant hash lookups.
  • Shallow Copy Warning: dict.copy() only clones the top-level container; nested sub-dictionaries share pointer addresses!

02 / Syntax Anatomy & Core Operations

PEP 584 MergingPython 3.9+
# 1. Binary merge operator (|): produces new dict
defaults = {"host": "localhost", "port": 8000}
overrides = {"port": 443, "ssl": True}
config = defaults | overrides
# Result: {'host': 'localhost', 'port': 443, 'ssl': True}

# 2. In-place update operator (|=): mutates defaults in place
defaults |= overrides

Clean, readable, and type-safe. Right-hand operand resolves all key collisions cleanly.

Accumulator & DeletionO(1) Mechanics
# 1. setdefault: query and lazy insertion in one lookup
catalog = {}
catalog.setdefault("electronics", []).append("laptop")

# 2. Safe pop with fallback default
val = catalog.pop("apparel", [])  # Returns [], no KeyError!

# 3. LIFO popitem: removes last inserted pair
catalog["books"] = ["python-guide"]
last_pair = catalog.popitem()  # ('books', ['python-guide'])

setdefault() avoids double-lookup performance overhead when accumulating values.

03 / CPython Execution Architecture: Shallow vs. Deep Merging

The Critical Blind Spot: Why | and .update() Obliterate Nested Sub-Trees

Both Python's .update() method and PEP 584's | operator perform shallow merges. They operate strictly on the top-level keys. If both dictionaries contain a nested dictionary under the same key, the right-hand child completely replaces the left-hand child, destroying all non-conflicting inner keys!

The Shallow Merge Blind Spot (base | overrides)
base = {"db": {"host": "localhost", "port": 5432}}
override = {"db": {"port": 9999}}

merged = base | override

Result:
merged = {"db": {"port": 9999}}
# FATAL: 'host' was completely deleted from memory!
The Hardened Recursive Deep-Merge Pattern
def deep_merge(base: dict, overrides: dict) -> dict:
    merged = base.copy()
    for k, v in overrides.items():
        if k in merged and isinstance(merged[k], dict) and isinstance(v, dict):
            merged[k] = deep_merge(merged[k], v)
        else:
            merged[k] = v
    return merged

Result:
{"db": {"host": "localhost", "port": 9999}} # Preserved!

1. Memory Allocation

d1 | d2 allocates a brand new dictionary table sized to hold len(d1) + len(d2) entries.

2. Pointer Copying

Values are copied by object reference. Nested lists or dicts still share pointers unless deepcopy() is run.

3. Right-Hand Precedence

CPython iterates over d2, overwriting existing hash slots if hash(k) matches an entry in d1.

04 / Method Matrix: Comparing Mutation & Merging Approaches

Method / OperatorMutates In Place?ReturnsCollision PolicyIdeal Use Case
d1 | d2 (PEP 584)NoNew dictRight operand winsPure functions, config merging where inputs must remain immutable.
d1 |= d2Yes (mutates d1)None (or d1)Right operand winsMemory-efficient in-place batch updates on large active caches.
d.update(other)Yes (mutates d)NoneArgument winsLegacy in-place update, accepts keyword arguments d.update(a=1).
d.setdefault(k, v)ConditionalValue at keyExisting key untouchedSingle-pass grouping into lists/sets without double lookup.
copy.deepcopy(d)NoCloned dictN/ADeeply isolated replication of nested configuration trees.

05 / Interactive Syntax Reference Sandbox

PEP 584 Union Operators (| and |=)

Comparing immutable binary merge d1 | d2 with in-place augmented update d1 |= d2:

Run PEP 584 merge operators
base = {"host": "localhost", "port": 8000, "debug": True}
overrides = {"port": 443, "ssl": True}

# 1. Binary merge (|): creates brand new dictionary
merged = base | overrides
print("Merged:", merged)
print("Base untouched:", base["port"])  # Still 8000!

# 2. In-place merge (|=): mutates base directly
base |= overrides
print("Base after |=:", base)

06 / Tiered Walkthroughs: From Foundation to Mastery

Level 1 · Foundation

Cloud Microservice Configuration Merger

Difficulty: Beginner

Problem: Build a deployment configuration resolution engine for containerized microservices. Settings are defined across three hierarchical layers: Global Defaults, Cluster Region Overrides, and Container Local Overrides. Merge these layers using modern PEP 584 operators so that more specific layers overwrite broader defaults.

Walkthrough: Cloud Microservice Configuration Merger Run cleanly to earn +1 XP
def resolve_service_deployment(
    global_defaults: dict,
    cluster_overrides: dict,
    container_local: dict
) -> dict:
    """
    Merges configuration layers with precedence:
    container_local > cluster_overrides > global_defaults.
    """
    # Chain the merge operator '|'; rightmost operand takes precedence on conflict
    final_config = global_defaults | cluster_overrides | container_local
    return final_config

defaults = {
    "image": "pylabs/base:3.12",
    "replicas": 2,
    "timeout_sec": 30,
    "enable_ssl": True,
    "region": "global"
}

cluster = {
    "replicas": 10,
    "region": "us-east-1",
    "vpc_id": "vpc-08129a"
}

container = {
    "replicas": 16,               # Local scaling override
    "profiler_enabled": True
}

print("=" * 65)
print(f"{'CONTAINER DEPLOYMENT CONFIGURATION RESOLVER':^65}")
print("=" * 65)

resolved = resolve_service_deployment(defaults, cluster, container)

for param, val in resolved.items():
    print(f"{param:<22}: {val}")
print("=" * 65)

assert resolved["replicas"] == 16          # Container won
assert resolved["region"] == "us-east-1"   # Cluster won
assert resolved["enable_ssl"] is True       # Default preserved
assert resolved["vpc_id"] == "vpc-08129a"   # Cluster addition preserved
print("Multi-Tier Layered Configuration Successfully Resolved!")
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.
Architectural Insight: This beginner example demonstrates modern Python 3.9+ dictionary merging using chaining: `defaults | cluster | container`. Unlike old nested `.update()` calls that mutated inputs, the `|` operator leaves all three input dictionaries untouched while producing an immutable result where right-hand values overwrite left-hand conflicts cleanly.
Level 2 · Applied

Multi-Category Financial Transaction Grouper with setdefault

Difficulty: Intermediate

Problem: Create an automated financial transaction ledger organizer. Given a stream of unorganized transaction dictionaries, partition and group transactions by category into lists. If a transaction lacks a category, assign it to 'Uncategorized'. Ensure the grouping runs in a single pass without redundant dictionary lookups.

Walkthrough: Multi-Category Financial Transaction Grouper with setdefault Run cleanly to earn +1 XP
def organize_transactions_by_category(transactions: list[dict]) -> dict[str, list[dict]]:
    """
    Groups transactions by category using the setdefault accumulator idiom.
    """
    grouped_ledger = {}
    
    for tx in transactions:
        # Extract category with safe fallback
        category = tx.get("category", "Uncategorized")
        
        # Single-pass accumulator: initializes empty list if missing, returns reference
        grouped_ledger.setdefault(category, []).append({
            "tx_id": tx["id"],
            "amount": tx["amount"],
            "vendor": tx.get("vendor", "Unknown")
        })
        
    return grouped_ledger

raw_transactions = [
    {"id": "TX101", "category": "Infrastructure", "amount": 420.00, "vendor": "AWS"},
    {"id": "TX102", "category": "Marketing", "amount": 1500.00, "vendor": "Google"},
    {"id": "TX103", "category": "Infrastructure", "amount": 85.00, "vendor": "Cloudflare"},
    {"id": "TX104", "amount": 24.50, "vendor": "Coffee Shop"},  # Missing category
    {"id": "TX105", "category": "Marketing", "amount": 300.00, "vendor": "Meta"}
]

print("=" * 65)
print(f"{'FINANCIAL TRANSACTION CATEGORY ACCUMULATOR':^65}")
print("=" * 65)

ledger = organize_transactions_by_category(raw_transactions)

for cat in sorted(ledger.keys()):
    items = ledger[cat]
    total_spent = sum(i["amount"] for i in items)
    print(f"Category: {cat:<16} | Entries: {len(items)} | Total: ${total_spent:.2f}")
    for it in items:
        print(f"   [{it['tx_id']}] {it['vendor']:<15} -> ${it['amount']:.2f}")
    print("-" * 65)
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.
Architectural Insight: This intermediate example demonstrates the `grouped_ledger.setdefault(category, []).append(...)` idiom. By calling `.setdefault()`, we avoid writing `if category not in grouped_ledger:` which requires two separate hash lookups. Instead, `.setdefault()` fetches or inserts the list reference in one pass, allowing `.append()` to push the record directly.
Level 3 · Architecture

Recursive Deep Merge Engine & Schema Diff Calculator

Difficulty: Advanced

Problem: Implement a production recursive deep-merge engine and schema diff analyzer. The engine must: 1) merge nested dictionaries recursively so that inner sub-dictionaries combine fields rather than being overwritten; 2) calculate a detailed diff report showing which keys were added, modified, or deleted between two configuration versions using dictionary views.

Walkthrough: Recursive Deep Merge Engine & Schema Diff Calculator Run cleanly to earn +1 XP
def deep_merge(base: dict, overrides: dict) -> dict:
    """
    Recursively merges overrides into base without destroying inner dictionaries.
    """
    merged = base.copy()
    for key, val in overrides.items():
        # If both are dictionaries, recurse down the tree
        if key in merged and isinstance(merged[key], dict) and isinstance(val, dict):
            merged[key] = deep_merge(merged[key], val)
        else:
            merged[key] = val
    return merged

def compute_dict_diff(old_dict: dict, new_dict: dict) -> dict:
    """
    Calculates top-level schema changes using dictionary view set algebra.
    """
    old_keys = old_dict.keys()
    new_keys = new_dict.keys()
    
    added_keys = sorted(new_keys - old_keys)
    removed_keys = sorted(old_keys - new_keys)
    common_keys = old_keys & new_keys
    
    modified_keys = sorted([
        k for k in common_keys if old_dict[k] != new_dict[k]
    ])
    
    return {
        "added": added_keys,
        "removed": removed_keys,
        "modified": modified_keys
    }

base_system_config = {
    "database": {
        "host": "db.internal.pylabs.org",
        "port": 5432,
        "pool": {"min": 5, "max": 20}
    },
    "telemetry": {"enabled": True, "sample_rate": 0.1},
    "legacy_flag": True
}

production_overrides = {
    "database": {
        "pool": {"max": 100}  # Should only modify pool.max!
    },
    "telemetry": {"sample_rate": 1.0},
    "new_audit_log": True
}

print("=" * 65)
print(f"{'RECURSIVE DEEP MERGE & SCHEMA DIFF CALCULATOR':^65}")
print("=" * 65)

merged_config = deep_merge(base_system_config, production_overrides)
print("Merged Database Settings:")
print(json.dumps(merged_config["database"], indent=2))
print("-" * 65)

diff = compute_dict_diff(base_system_config, merged_config)
print("Schema Evolution Diff:")
print(f"  Added Keys:    {diff['added']}")
print(f"  Removed Keys:  {diff['removed']}")
print(f"  Modified Keys: {diff['modified']}")
print("=" * 65)

# Verify deep merge integrity
assert merged_config["database"]["host"] == "db.internal.pylabs.org"
assert merged_config["database"]["port"] == 5432
assert merged_config["database"]["pool"]["min"] == 5
assert merged_config["database"]["pool"]["max"] == 100
print("Nested Sub-Dictionary Fields Successfully Preserved & Merged!")
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.
Architectural Insight: This advanced example solves one of the most notorious traps in Python dictionary merging: shallow overwrites. Using standard `base | overrides` on the database object would have replaced the entire database dictionary with `{'pool': {'max': 100}}`, discarding `host`, `port`, and `pool.min`. Our `deep_merge()` recursively inspects child sub-dictionaries, preserving sibling keys. The `compute_dict_diff()` function harnesses view set operations `old_keys - new_keys` to detect added and removed fields in $O(N)$ time.

07 / Anti-Patterns & Production Pitfalls

Eager Default Evaluation in setdefault()

Defective Code Pattern
def fetch_data():
    print("Heavy network call!")
    return []

cache = {"key": [1, 2, 3]}
val = cache.setdefault("key", fetch_data())  # BUG: fetch_data() runs even though 'key' exists!

Error Behavior: Unnecessary CPU cycles, database queries, or network requests triggered on every check.

Hardened Production Fix
# Check membership explicitly if the fallback is computationally expensive:
if "key" not in cache:
    cache["key"] = fetch_data()
val = cache["key"]
Why It Happens: Python evaluates all function arguments BEFORE passing them into setdefault().

Shallow Copy Mutability Leaks

Defective Code Pattern
orig = {"meta": {"env": "prod"}}
clone = orig.copy()
clone["meta"]["env"] = "staging"  # BUG: Mutates orig['meta']['env'] as well!

Error Behavior: Silent state pollution: changes to clones corrupt original production settings.

Hardened Production Fix
import copy
clone = copy.deepcopy(orig)  # Completely recursive, independent clone
Why It Happens: dict.copy() is shallow; nested mutable objects (dicts, lists) share identical memory addresses.

Assuming the Merge Operator | Performs Deep Merges

Defective Code Pattern
d1 = {"db": {"host": "localhost", "port": 5432}}
d2 = {"db": {"port": 9999}}
merged = d1 | d2  # BUG: 'host' is completely deleted! merged['db'] is only {'port': 9999}

Error Behavior: Silent data loss: nested dictionary keys from the left operand are completely lost.

Hardened Production Fix
# Use a recursive deep merge function for nested dictionary trees:
merged = deep_merge(d1, d2)
Why It Happens: PEP 584's | operator only merges top-level keys; it overwrites nested dictionaries entirely.

Modifying Dictionaries During View Iteration

Defective Code Pattern
users = {"a": 1, "b": 2, "c": 3}
for k, v in users.items():
    if v == 2:
        users.pop(k)  # BUG: RuntimeError during iteration!

Error Behavior: RuntimeError: dictionary changed size during iteration

Hardened Production Fix
# Snapshot target keys with a list comprehension first:
to_delete = [k for k, v in users.items() if v == 2]
for k in to_delete:
    users.pop(k)
Why It Happens: Dictionary views are dynamic. Modifying the underlying dictionary invalidates active iterators.

Reversed Operand Precedence in Merging

Defective Code Pattern
defaults = {"timeout": 30}
user_pref = {"timeout": 10}
# BUG: defaults overwrote user preferences!
config = user_pref | defaults

Error Behavior: Defaults take precedence over custom overrides.

Hardened Production Fix
config = defaults | user_pref  # Rightmost operand wins
Why It Happens: In d1 | d2, the rightmost operand ALWAYS wins conflicts.

Relying on popitem() for FIFO Queue Behavior

Defective Code Pattern
q = {"task1": "a", "task2": "b"}
first_task = q.popitem()  # BUG: Returns ('task2', 'b'), NOT ('task1', 'a')!

Error Behavior: Processes items in reverse LIFO (stack) order instead of FIFO (queue) order.

Hardened Production Fix
# For FIFO queue behavior, use collections.deque or pop the specific key:
first_key = next(iter(q))
first_val = q.pop(first_key)
Why It Happens: popitem() removes the LAST inserted item (LIFO).

08 / Production Systems & Industry Architecture

Developer Productivity

🟢 Simple Script: Local Developer Automation Tool

Individual developers and IT administrators automate everyday repetitive workflows.

config = {"timeout": 30}
retries = config.get("retries", 3)  # Safe lookup with default
print("Retries setting:", retries)

A clean standalone script providing fast, local automation with zero dependencies.

Cloud Microservices

🟡 Scaled Service: Backend API Service Integration

SaaS platforms run backend microservices processing requests concurrently.

# Service integration layer
def handle_request(payload: dict) -> dict:
    return {"status": "success", "processed": True}

A modular service handler integrating validation and structured responses.

Distributed Systems

🔴 Enterprise Production: High-Throughput Cloud Architecture

Microservice configuration loaders safely read nested JSON dictionaries with fallbacks to guarantee fault tolerance during deploys.

# High-resilience production pattern
import time

def process_stream(events: list) -> int:
    processed = 0
    for event in events:
        if event:
            processed += 1
    return processed

Microservice configuration loaders safely read nested JSON dictionaries with fallbacks to guarantee fault tolerance during deploys.

09 / Engineering Case Study: Multi-Tier Config Loading & Schema Evolution

Microservice Architecture: Cascading Config Overrides & Schema Drift Detection

Modern containerized cloud platforms resolve application configuration dynamically across multiple infrastructure tiers: internal container defaults → cluster Helm chart values → per-environment ConfigMaps → secret managers.

1. Chained PEP 584 Cascades

Chaining resolved = defaults | cluster_cfg | env_vars | cli_args provides clean, readable precedence where later operands overwrite earlier layers without mutating shared templates in place.

2. Automated Schema Drift Diffing

Before applying hot reloads, systems diff deployed configurations against active runtime state using view set algebra: v_new.keys() - v_old.keys() identifies new feature gates, whilev_old.keys() - v_new.keys() flags deprecated flags pending clean teardown.

10 / Guided Labs: Hands-On Implementation

Lab 1

Session State Updater & Blacklist Purger

Difficulty: Easy

Write a function `manage_session(session: dict, updates: dict, expire_keys: list[str]) -> dict` that: 1) updates `session` in place with all key-value pairs from `updates` using the `|=` operator; 2) removes each key listed in `expire_keys` using `.pop(key, None)` so that non-existent keys do not raise errors; 3) returns the mutated session dictionary.

Implementation Hints
  • Use the in-place merge operator session |= updates.
  • Loop over expire_keys and call session.pop(k, None) to avoid KeyError.
Starter Workspace
Lab 1 Starter Code
def manage_session(session: dict, updates: dict, expire_keys: list[str]) -> dict:
    # TODO: Update session in-place and purge expired keys
    pass

# Test:
# s = {"id": "s1", "ip": "1.1.1.1", "auth": True}
# manage_session(s, {"ip": "2.2.2.2", "role": "admin"}, ["auth", "missing"])
# -> {"id": "s1", "ip": "2.2.2.2", "role": "admin"}
Verification Test Cases
manage_session({'id': 's1', 'ip': '1.1.1.1', 'auth': True}, {'ip': '2.2.2.2', 'role': 'admin'}, ['auth', 'missing'])→ {'id': 's1', 'ip': '2.2.2.2', 'role': 'admin'}
Lab 2

Email Domain Grouping with setdefault

Difficulty: Medium

Write a function `group_emails_by_domain(emails: list[str]) -> dict[str, list[str]]` that parses a list of email addresses and groups usernames by domain. The keys must be lowercase domain names, and the values must be sorted lists of lowercase usernames belonging to each domain. Ignore strings that do not contain an '@' character.

Implementation Hints
  • Clean and normalize each address with email.strip().lower().
  • Split on '@' using clean.split('@', 1).
  • Use domain_map.setdefault(domain, []).append(username) to accumulate names.
  • Sort the username lists in a dictionary comprehension before returning.
Starter Workspace
Lab 2 Starter Code
def group_emails_by_domain(emails: list[str]) -> dict[str, list[str]]:
    # TODO: Group usernames by domain
    pass

# Test:
# emails = ["Alice@pylabs.org", "Bob@gmail.com", "carol@pylabs.org", "dave@gmail.com"]
# group_emails_by_domain(emails) -> {"gmail.com": ["bob", "dave"], "pylabs.org": ["alice", "carol"]}
Verification Test Cases
group_emails_by_domain(['Alice@pylabs.org', 'Bob@gmail.com', 'carol@pylabs.org', 'dave@gmail.com'])→ {'gmail.com': ['bob', 'dave'], 'pylabs.org': ['alice', 'carol']}

11 / Independent Practice Tasks

Task 1 • Easy

Bidirectional Dict Inverter

Write a function invert_mapping(d: dict[str, str]) -> dict[str, str] that reverses keys and values. If duplicate values exist, raise a ValueError to ensure strict bijection.

Target: Bijection validation + comprehension
Task 2 • Medium

Event Partition Accumulator

Build a stream batching processor. Given an event list with severity levels, use setdefault() to partition events into a dictionary mapping severity to lists of payloads.

Target: Single-pass setdefault grouping
Task 3 • Hard

Recursive Deep Config Flattener

Implement a multi-level JSON merger and flattener. Accept arbitrary nested dictionary depths, merge them without destructive overwriting, and emit dot-notated environment strings.

Target: Recursive tree traversal + merge

12 / Key Takeaways & Quick Cheat Sheet

Core Invariants & Rules

  • In Python 3.9+, use the merge operator d1 | d2 (returns a new dict) and in-place update d1 |= d2 for clean dictionary combination.
  • In d1 | d2, the rightmost operand wins in the event of key collisions.
  • d.setdefault(key, default) provides a single-pass lookup-and-insert mechanism, but eagerly evaluates its default argument.
  • d.pop(key, [default]) removes the key and returns its value or default; missing keys without defaults raise KeyError.
  • d.copy() is shallow; use copy.deepcopy(d) whenever dictionaries contain nested mutable lists or sub-dictionaries.
  • d.keys() and d.items() views support set operations (&, |, -, ^) for high-speed schema diffing.
  • Standard merging operators (| and .update()) perform shallow merges; nested sub-dictionaries require recursive deep merging.

Cheat Sheet Reference

d1 | d2 — Merge dictionaries into a new dict (right operand wins).
d1 |= d2 — In-place dictionary update.
d.update(other, **kwargs) — Mutate dictionary with mapping or kwargs.
d.setdefault(k, default) — Return d[k] or set d[k] = default.
d.pop(k, [default]) — Remove k and return value (or default).
d.popitem() — Remove and return last inserted (k, v) pair in LIFO order.
d.clear() — Empty dictionary in place.
d1.items() ^ d2.items() — Symmetric difference between two dictionary states.
copy.deepcopy(d) — Fully recursive independent clone of nested dictionary.

13 / Interactive Quiz Assessment (17 Questions)

Answered: 0 / 17
Question 1 of 17Kind: concept • easy
What does dict.get(key, default) return if key is NOT in dict?
Question 2 of 17Kind: predict-output • easy
What does this print?
d = {"a": 1}
print(d.get("b", 42))
Question 3 of 17Kind: predict-output • easy
What does this print?
data = {"x": 10, "y": 20}
print(len(data.keys()))
Question 4 of 17Kind: concept • easy
What does dict.items() yield during iteration?
Question 5 of 17Kind: predict-output • medium
What does this print?
d = {"p": 100}
val = d.pop("p")
print(val, len(d))
Question 6 of 17Kind: predict-output • medium
What does this print?
tracker = {}
tracker["views"] = tracker.get("views", 0) + 1
print(tracker["views"])
Question 7 of 17Kind: spot-the-bug • medium
Which line has a bug when iterating over key-value pairs of dictionary d?
Question 8 of 17Kind: predict-output • medium
What does this print?
d1 = {"a": 1}
d2 = {"b": 2}
d1.update(d2)
print(len(d1))
Question 9 of 17Kind: syntax • hard
Which method inserts a key with a default value only if the key is not already present?
Question 10 of 17Kind: predict-output • hard
What does this print?
chars = "aab"
freq = {}
for c in chars:
    freq[c] = freq.get(c, 0) + 1
print(freq["a"])
Question 11 of 17Kind: concept • easy
In Python 3.9+, what does the expression d1 | d2 evaluate to?
Question 12 of 17Kind: concept • medium
What is the difference between d1 | d2 and d1 |= d2?
Question 13 of 17Kind: concept • hard
Why is d.setdefault(k, expensive_func()) considered a performance hazard?
Question 14 of 17Kind: concept • hard
What does dict_a.items() ^ dict_b.items() compute?
Question 15 of 17Kind: concept • easy
What does d.pop('missing') do if key 'missing' does not exist and no default argument is given?
Question 16 of 17Kind: concept • medium
What is the difference between d.copy() and copy.deepcopy(d)?
Question 17 of 17Kind: predict-output • medium
What does this print?
d = {'a': 1, 'b': 2}
print(d.setdefault('a', 100))
print(d['a'])

14 / Study Guide & Technical FAQ

Q1:What is the difference between d1.update(d2) and d1 | d2?

d1.update(d2) mutates d1 in place and returns None. In contrast, d1 | d2 creates and returns a brand new dictionary without modifying either d1 or d2. If you want in-place merging using operator syntax, use d1 |= d2.

Q2:Why did my nested dictionary get overwritten when I used d1 | d2?

The | operator is a shallow merger. If both d1 and d2 contain a sub-dictionary with the same key (e.g. 'database': {...}), d2's sub-dictionary completely replaces d1's sub-dictionary instead of combining their inner keys. Use a recursive deep-merge function for nested dictionary trees.

Q3:Why is d.setdefault(key, []) better than an if-not-in check?

Writing if key not in d: d[key] = [] followed by d[key].append(x) hashes and searches for key twice. d.setdefault(key, []).append(x) executes the search and insertion in a single C-level operation, cutting lookup overhead in half.

Q4:How can I compare two dictionaries to see what changed?

Use dictionary view set operations: d1.items() & d2.items() gives unchanged pairs, d2.items() - d1.items() gives new or modified pairs, and d1.keys() - d2.keys() gives deleted keys.

Worked example

Safely read an optional setting from user configuration with a default fallback.

Example
config = {"theme": "dark"}
lang = config.get("language", "en-US")
print(f"Language: {lang}")

How it works, step by step

  1. .get(key, default) avoids crashing with KeyError.
  2. If key exists, its value is returned.
  3. If missing, the fallback 'en-US' is returned.

Your notes

Sign in to keep private notes alongside this lesson.

Sign in to take notes

Dictionary Methods & Key Merging practice

Sign-in required

Given profile = {"username": "neo", "role": "operator"}, safely retrieve "email" using .get() with fallback "missing" into a variable email. Update role to "architect" using .update(). Print Email: missing, Role: architect.

Coding Practice is Locked

Sign in to access the interactive Python editor, write and run code checks against automated tests, use hints, and earn XP.

Practice quiz

Check yourself

15 questions with explanations after submission, so you can correct course as you go.

Dictionary Methods & Key Merging quiz

Locked

You must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.

Lesson incomplete

Not completed

Create an ID or sign in to save lesson completion across devices.

Create ID to save
Module lesson progress0/6 complete
Module cheat sheet