Dictionaries
📚 Dictionaries
Dictionaries are arguably Python's most important and versatile data structure. They store key-value pairs, are ordered (Python 3.7+), and map directly onto JSON — making them essential for web development. Almost every real Python program uses them heavily.
{} syntax and store key-value pairs. Unlike JS, you must use d["key"] bracket notation — there's no dot access (d.key). Unlike Java's HashMap, dicts are built in with no import, are ordered by insertion (Python 3.7+), and have a much cleaner API. The JS spread operator {...a, ...b} has a Python equivalent: {**a, **b}.
.items()), safe access (.get()), and dict comprehensions is essential for writing clean FastAPI route handlers.
🏗️ Creating & Accessing Dictionaries
dict() is an alternative constructor useful when keys are valid identifiers.d[key] raises a KeyError if the key is missing. d.get(key) returns None (or a default you provide) — almost always the safer choice in real code..setdefault() to set a value only if the key is absent — useful for building nested structures.del removes a key (raises KeyError if missing). .pop(key) removes and returns the value — like lists. .popitem() removes and returns the last inserted pair as a tuple.in checks keys by default — not values. This is an O(1) operation (hash lookup), making dicts excellent for fast lookups regardless of size..copy() or dict(d) for a shallow copy. For deeply nested dicts, use copy.deepcopy().🔄 Iterating Dictionaries
Dicts offer three views for iteration. You'll use .items() most often — it's the workhorse of dict iteration in real code.
🔗 Merging Dictionaries
Python offers three ways to merge dicts. The | operator (Python 3.9+) is the cleanest. The ** unpacking approach works in older versions and is common in FastAPI when building response objects.
{...a, ...b}. Works in Python 3.5+.⚡ Dictionary Comprehensions
Just like list comprehensions, but producing a dict. The pattern is { key_expr: value_expr for item in iterable if condition }. Essential for transforming and filtering dicts in FastAPI handlers.
inventory = {
"milk": {"qty": 2, "days_left": 2},
"eggs": {"qty": 6, "days_left": 10},
"yogurt": {"qty": 1, "days_left": 1},
"butter": {"qty": 0, "days_left": 14},
}
# 1. Filter — only items expiring within 3 days
expiring = {
name: data
for name, data in inventory.items()
if data["days_left"] <= 3
}
# → {"milk": {...}, "yogurt": {...}}
# 2. Transform — extract just the quantities
quantities = {name: data["qty"] for name, data in inventory.items()}
# → {"milk": 2, "eggs": 6, "yogurt": 1, "butter": 0}
# 3. Invert a dict — swap keys and values
day_to_meal = {"Mon": "Pasta", "Tue": "Curry", "Wed": "Soup"}
meal_to_day = {meal: day for day, meal in day_to_meal.items()}
# → {"Pasta": "Mon", "Curry": "Tue", "Soup": "Wed"}
# 4. Build a lookup dict from a list
names = ["milk", "eggs", "butter"]
in_stock = {name: ("yes" if name in inventory else "no") for name in names}
# → {"milk": "yes", "eggs": "yes", "butter": "yes"}
💻 Examples in Context
.get() with a default is so common in Python web code that it's worth seeing several real-world variants.recipe = {
"name": "Risotto",
"servings": 4,
"tags": ["vegetarian", "gluten-free"],
}
# Safe access with fallback
desc = recipe.get("description", "No description available")
calories = recipe.get("calories", 0)
tags = recipe.get("tags", []) # empty list if tags absent
# Common pattern: guard before deep access
nutrition = recipe.get("nutrition")
if nutrition:
protein = nutrition.get("protein_g", 0)
# setdefault — ensure a key exists before appending
meal_plan = {}
meal_plan.setdefault("Monday", []).append("Risotto")
meal_plan.setdefault("Monday", []).append("Salad")
# → {"Monday": ["Risotto", "Salad"]}
# setdefault only sets if key absent — won't reset to []
inventory = {
"milk": {"qty": 2, "unit": "litres", "days_left": 2},
"eggs": {"qty": 6, "unit": "units", "days_left": 10},
"yogurt": {"qty": 1, "unit": "pot", "days_left": 1},
"chicken": {"qty": 500, "unit": "grams", "days_left": 0},
}
alerts = []
for name, data in inventory.items():
days = data["days_left"]
if days <= 0:
alerts.append(f"🚫 {name} has EXPIRED — discard immediately")
elif days <= 3:
alerts.append(f"⚠ {name} expires in {days} day(s) — use soon!")
for alert in alerts:
print(alert)
# 🚫 chicken has EXPIRED — discard immediately
# ⚠ yogurt expires in 1 day(s) — use soon!
# ⚠ milk expires in 2 day(s) — use soon!
[] or .get() calls.meal_plan = {
"Monday": {
"breakfast": "Porridge",
"lunch": "Chicken Soup",
"dinner": "Pasta Bolognese",
},
"Tuesday": {
"breakfast": "Toast",
"lunch": "Caesar Salad",
"dinner": "Chicken Tikka",
},
}
# Access nested value
monday_dinner = meal_plan["Monday"]["dinner"]
# → "Pasta Bolognese"
# Safe nested access with .get()
wednesday = meal_plan.get("Wednesday", {})
lunch = wednesday.get("lunch", "Not planned")
# → "Not planned" (no KeyError)
# Iterate nested structure
for day, meals in meal_plan.items():
print(f"\n{day}:")
for meal_type, dish in meals.items():
print(f" {meal_type.capitalize()}: {dish}")
setdefault trick is the cleanest way to do this without importing anything.ingredients = [
{"name": "milk", "category": "dairy"},
{"name": "yogurt", "category": "dairy"},
{"name": "spinach", "category": "produce"},
{"name": "carrots", "category": "produce"},
{"name": "chicken", "category": "meat"},
]
# Group into dict by category
by_category = {}
for item in ingredients:
cat = item["category"]
by_category.setdefault(cat, []).append(item["name"])
# → {
# "dairy": ["milk", "yogurt"],
# "produce": ["spinach", "carrots"],
# "meat": ["chicken"]
# }
for category, items in by_category.items():
print(f"{category}: {', '.join(items)}")
import json
recipe = {
"id": 42,
"name": "Pasta Bolognese",
"servings": 4,
"tags": ["italian", "meat"],
"nutrition": {"calories": 520, "protein_g": 28},
}
# Python dict → JSON string
json_str = json.dumps(recipe, indent=2)
print(json_str)
# {
# "id": 42,
# "name": "Pasta Bolognese",
# ...}
# JSON string → Python dict
data = json.loads(json_str)
print(data["name"]) # "Pasta Bolognese"
# In FastAPI a route handler looks like this:
# @app.get("/recipes/{recipe_id}")
# def get_recipe(recipe_id: int):
# return {"id": recipe_id, "name": "Pasta"} ← dict auto-converts to JSON
📋 Quick Reference
| Operation | Syntax | Notes |
|---|---|---|
| Create | {"key": val} | Keys must be hashable (str, int, tuple — not list) |
| From pairs | dict([("a",1),("b",2)]) | From a list of 2-tuples |
| Get value (unsafe) | d["key"] | Raises KeyError if missing |
| Get value (safe) | d.get("key", default) | Returns default (None) if missing — prefer this |
| Set / update | d["key"] = val | Creates if absent, overwrites if present |
| Set if absent | d.setdefault("key", val) | Won't overwrite existing value |
| Remove | del d["key"] | Raises KeyError if missing |
| Remove & return | d.pop("key", default) | Safe with default — like list.pop() |
| Remove last | d.popitem() | Returns (key, value) tuple |
| Clear all | d.clear() | |
| Key exists | "key" in d | O(1) hash lookup — checks keys only |
| Length | len(d) | Number of key-value pairs |
| All keys | d.keys() | Returns a view — not a list |
| All values | d.values() | Returns a view — not a list |
| All pairs | d.items() | Returns (key, value) view — use in for loops |
| Shallow copy | d.copy() | Or dict(d) or {**d} |
| Merge (new) | d1 | d2 | Python 3.9+ — right dict wins conflicts |
| Merge (new) | {**d1, **d2} | Python 3.5+ — right dict wins conflicts |
| Merge (in place) | d1.update(d2) | Mutates d1 |
| Comprehension | {k: v for k, v in d.items()} | Transform or filter a dict |
| To JSON | json.dumps(d) | Requires import json |
| From JSON | json.loads(s) | Returns a Python dict |
d[key] raises KeyError — use .get() — the biggest cause of crashes in Python dict code. In JavaScript, accessing a missing key just gives
undefined. In Python it raises an exception. Always use d.get(key) when the key might not exist. Get into the habit early.in checks keys, not values —
"Pasta" in recipe checks if "Pasta" is a key, not a value. If you want to check values, use "Pasta" in recipe.values() — though this is O(n), not O(1)..keys(), .values(), .items() return views, not lists — they are live views of the dict, not snapshots. If you modify the dict while holding a view, the view reflects the change. If you need a true list, wrap them:
list(d.keys()). Also, you cannot use index access on views: d.keys()[0] raises a TypeError.No dot access like JavaScript — in JS you can write
recipe.name. In Python you must write recipe["name"] or recipe.get("name"). If you want dot access, look at SimpleNamespace or Pydantic models — which you'll encounter in FastAPI.Dict keys are case-sensitive —
d["Name"] and d["name"] are different keys. This catches people when parsing JSON from external APIs where key casing is inconsistent.