Python dictionaries are the backbone of web development with Python — every JSON request body is parsed into a dict, every SQLAlchemy model instance can be exported as a dict, and FastAPI’s response serialisation works by converting your Pydantic models to dicts before encoding them as JSON. Dictionaries store data as key-value pairs with O(1) average-time lookup by key, and since Python 3.7, they maintain insertion order. Understanding the full dict API — safe access, merging, iteration, and the many transformation patterns — is essential for working effectively with FastAPI’s request and response data.
Creating and Accessing Dictionaries
# Create with curly braces
user = {"name": "Alice", "age": 30, "role": "admin"}
# Create with dict() constructor
post = dict(title="My Post", published=True, views=0)
# Create from a list of tuples
config = dict([("host", "localhost"), ("port", 5432)])
# Empty dict
empty = {}
# Accessing values
user["name"] # "Alice" — raises KeyError if key missing
# Safe access with get() — returns None (or default) if key missing
user.get("name") # "Alice"
user.get("email") # None — no KeyError
user.get("email", "N/A") # "N/A" — custom default
# Check if key exists
"name" in user # True
"email" in user # False
"admin" in user.values() # True — check values
# Get all keys, values, items
user.keys() # dict_keys(["name", "age", "role"])
user.values() # dict_values(["Alice", 30, "admin"])
user.items() # dict_items([("name", "Alice"), ("age", 30), ("role", "admin")])
.get() instead of direct bracket access when the key might not exist. Direct access (d["key"]) raises a KeyError if the key is missing — a common crash in web applications when optional request fields are not included. The pattern d.get("field", default_value) is safe and explicit. In FastAPI, Pydantic models handle this automatically, but when working with raw dicts from database queries, always use .get() for optional fields.dict.copy() for a shallow copy or copy.deepcopy(d) for a deep copy of nested structures. The {**existing} spread syntax also creates a shallow copy while allowing field additions or overrides.Modifying Dictionaries
user = {"name": "Alice", "age": 30}
# Add or update a single key
user["email"] = "alice@example.com" # add new key
user["age"] = 31 # update existing key
# update() — add/update multiple keys at once
user.update({"role": "admin", "age": 32})
user.update(role="editor") # keyword argument form
# setdefault() — add key only if it doesn't exist
user.setdefault("is_active", True) # adds is_active: True
user.setdefault("name", "Unknown") # does NOT overwrite existing "name"
print(user["name"]) # still "Alice"
# Delete keys
del user["age"] # raises KeyError if missing
popped = user.pop("role") # removes and returns value
user.pop("missing", None) # safe delete — returns None if missing
# Clear all keys
user.clear() # {} — empty dict
# Merge dicts — create new dict with combined keys
base = {"a": 1, "b": 2}
extra = {"b": 20, "c": 3}
# Python 3.9+ merge operator
merged = base | extra # {"a": 1, "b": 20, "c": 3} — extra wins on conflict
# All Python 3 — spread into new dict
merged = {**base, **extra} # same result
# Conditional update
update_data = {"name": "Bob", "bio": None}
# Only update keys where value is not None
user = {k: v for k, v in {**user, **update_data}.items() if v is not None}
Iterating Over Dictionaries
post = {"id": 1, "title": "My Post", "views": 150, "published": True}
# Iterate over keys (default)
for key in post:
print(key) # id, title, views, published
# Iterate over values
for value in post.values():
print(value) # 1, "My Post", 150, True
# Iterate over key-value pairs (most common)
for key, value in post.items():
print(f"{key}: {value}")
# Build a new dict from iteration (dict comprehension)
# Exclude sensitive fields
safe_post = {k: v for k, v in post.items() if k != "author_password_hash"}
# Transform all values
upper_keys = {k.upper(): v for k, v in post.items()}
# {"ID": 1, "TITLE": "My Post", "VIEWS": 150, "PUBLISHED": True}
# Filter by value type
text_only = {k: v for k, v in post.items() if isinstance(v, str)}
# {"title": "My Post"}
Nested Dictionaries — FastAPI Response Patterns
# Nested dict — common in API responses
response = {
"success": True,
"data": {
"id": 1,
"title": "My Post",
"author": {
"id": 42,
"name": "Alice",
},
"tags": ["python", "fastapi"],
},
"meta": {
"total": 100,
"page": 1,
"limit": 10,
}
}
# Access nested values
response["data"]["author"]["name"] # "Alice"
response.get("data", {}).get("author", {}).get("name") # safer chain
# Update nested value
response["data"]["title"] = "Updated Title"
# Add field to nested dict
response["data"]["slug"] = "my-post"
# Flatten nested for database storage
def flatten_user(user_dict: dict) -> dict:
return {
"user_id": user_dict["id"],
"user_name": user_dict["name"],
"user_email": user_dict.get("email", ""),
}
Common Mistakes
Mistake 1 — KeyError from direct access on optional key
❌ Wrong — crashes if key is absent:
bio = user["bio"] # KeyError if "bio" not in user
✅ Correct — use get() with a default:
bio = user.get("bio", "") # "" if bio is missing ✓
Mistake 2 — Mutating a dict while iterating over it
❌ Wrong — RuntimeError: dictionary changed size during iteration:
for key in d:
if key.startswith("_"):
del d[key] # RuntimeError!
✅ Correct — iterate over a copy of the keys:
for key in list(d.keys()):
if key.startswith("_"):
del d[key] # ✓ safe
Mistake 3 — Using mutable value as dict default in get()
❌ Wrong — same list object returned as default every time:
items = d.get("items", [])
items.append("x") # modifies the default [] object — or does it?
# Actually safe with get() since a new [] is created each call.
# But don't rely on this — use defaultdict instead.
✅ Correct for accumulating — use defaultdict (covered in Lesson 5).
Quick Reference
| Operation | Code |
|---|---|
| Create | {"key": "value"} or dict(key="value") |
| Safe access | d.get("key", default) |
| Add/update key | d["key"] = value |
| Update many | d.update({"k": v}) |
| Add if missing | d.setdefault("key", default) |
| Delete key | del d["key"] or d.pop("key", None) |
| Merge (new dict) | {**d1, **d2} or d1 | d2 (3.9+) |
| Iterate pairs | for k, v in d.items(): |
| Check key | "key" in d |
| All keys/values | d.keys(), d.values() |