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.

⚡ Coming from Java / JavaScript If you know JavaScript objects, Python dicts will feel very familiar — with some key differences. Like JS objects, Python dicts use {} 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}.
🚀 FastAPI relevance — CRITICAL JSON is the language of web APIs, and Python dicts are JSON. Every FastAPI response you return, every request body you receive, and every database record you work with will involve dicts. Understanding dict iteration (.items()), safe access (.get()), and dict comprehensions is essential for writing clean FastAPI route handlers.

🏗️ Creating & Accessing Dictionaries

Creating
{ key: value, ... }
Keys must be hashable — strings, numbers, and tuples work. Lists do not. Values can be anything. dict() is an alternative constructor useful when keys are valid identifiers.
# Literal syntax recipe = { "name": "Pasta", "servings": 4, "vegetarian": True, "ingredients": ["pasta", "tomato"], } # dict() constructor item = dict(name="milk", qty=2) # Empty dict inventory = {}
Accessing values
d[key] vs d.get(key)
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.
# Bracket access — raises KeyError if missing name = recipe["name"] # "Pasta" # recipe["missing"] ← KeyError! # .get() — safe, returns None or default desc = recipe.get("description") # → None (no KeyError) desc = recipe.get("description", "No description") # → "No description"
Adding & updating
d[key] = value
Assignment creates the key if it doesn't exist, or overwrites the value if it does. Use .setdefault() to set a value only if the key is absent — useful for building nested structures.
# Add a new key recipe["prep_mins"] = 30 # Update an existing key recipe["servings"] = 6 # Only set if key doesn't already exist recipe.setdefault("calories", 0) # Won't overwrite if "calories" exists
Removing entries
del / .pop() / .clear()
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.
# del — raises KeyError if missing del recipe["calories"] # .pop() — returns the removed value servings = recipe.pop("servings") servings = recipe.pop("servings", 4) # default if missing # .popitem() — removes last inserted pair key, val = recipe.popitem() # .clear() — empties the dict recipe.clear()
Membership & length
in / not in / len()
in checks keys by default — not values. This is an O(1) operation (hash lookup), making dicts excellent for fast lookups regardless of size.
# Checking keys — O(1) lookup if "name" in recipe: print(recipe["name"]) if "calories" not in recipe: print("No calorie info") # Length — number of key-value pairs print(len(recipe)) # 3 # Check if empty if not recipe: print("Empty dict")
Copying
.copy() / dict()
Like lists, assignment copies the reference — both variables point to the same dict. Use .copy() or dict(d) for a shallow copy. For deeply nested dicts, use copy.deepcopy().
import copy # Shallow copy — safe for flat dicts recipe_copy = recipe.copy() recipe_copy = dict(recipe) # Deep copy — for nested dicts deep = copy.deepcopy(recipe) # ⚠ This is NOT a copy: # alias = recipe ← same object!

🔄 Iterating Dictionaries

Dicts offer three views for iteration. You'll use .items() most often — it's the workhorse of dict iteration in real code.

.keys()
Iterate over keys (also the default)
for key in recipe: print(key) # same as: for key in recipe.keys(): print(key) # → "name" # → "servings" # → "vegetarian"
.values()
Iterate over values only
for value in recipe.values(): print(value) # → "Pasta" # → 4 # → True
.items()
Iterate over key-value pairs ← use this most
for key, val in recipe.items(): print(f"{key}: {val}") # → name: Pasta # → servings: 4 # → vegetarian: True

🔗 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.

| operator — Python 3.9+
base = {"name": "Pasta"} extra = {"servings": 4} merged = base | extra # → {"name": "Pasta", # "servings": 4} # Update in place base |= extra
Cleanest syntax. Right dict wins on conflicts.
** unpacking — all versions
base = {"name": "Pasta"} extra = {"servings": 4} merged = {**base, **extra} # → {"name": "Pasta", # "servings": 4} # Add extra keys inline merged = {**base, "prep": 30}
Like JS {...a, ...b}. Works in Python 3.5+.
.update() — mutates in place
base = {"name": "Pasta"} extra = {"servings": 4} base.update(extra) # base is now modified # → {"name": "Pasta", # "servings": 4}
Modifies the original — use when you want that.

⚡ 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.

Dict Comprehension Patterns
Building, transforming, and filtering dicts in a single expression.
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

Safe Access Patterns — .get() in Practice
The pattern of using .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 []
Iterating with .items() — Expiry Alert System
The most common dict iteration pattern — unpack each key-value pair inside a for loop. This is the Python idiom you'll write hundreds of times.
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!
Nested Dictionaries — Structured Data
Dicts of dicts are the natural way to represent structured records in Python — and mirror JSON perfectly. Access nested values by chaining [] 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}")
Building Dicts Dynamically — Grouping Data
A common real-world pattern: grouping a flat list of items into a dict by some category. The 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)}")
Dicts as JSON — The FastAPI Connection
In FastAPI, returning a Python dict from a route automatically serialises it to JSON. Understanding dicts is understanding API responses.
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

OperationSyntaxNotes
Create{"key": val}Keys must be hashable (str, int, tuple — not list)
From pairsdict([("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 / updated["key"] = valCreates if absent, overwrites if present
Set if absentd.setdefault("key", val)Won't overwrite existing value
Removedel d["key"]Raises KeyError if missing
Remove & returnd.pop("key", default)Safe with default — like list.pop()
Remove lastd.popitem()Returns (key, value) tuple
Clear alld.clear()
Key exists"key" in dO(1) hash lookup — checks keys only
Lengthlen(d)Number of key-value pairs
All keysd.keys()Returns a view — not a list
All valuesd.values()Returns a view — not a list
All pairsd.items()Returns (key, value) view — use in for loops
Shallow copyd.copy()Or dict(d) or {**d}
Merge (new)d1 | d2Python 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 JSONjson.dumps(d)Requires import json
From JSONjson.loads(s)Returns a Python dict
⚠ Gotchas for Java / JavaScript Programmers:

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-sensitived["Name"] and d["name"] are different keys. This catches people when parsing JSON from external APIs where key casing is inconsistent.