Nested JSON requires defensive extraction at every level. This program takes a multi-level, step-by-step approach to reading the JSON instead of diving straight to the fields we want. Rather than assuming the full structure is present, it works through it in order: first it looks for the top-level "results" list, then it checks that there is at least one user in that list, then it looks inside that user for a "name" object, and finally it tries to read the "first" field from that object. At each step it pauses to ask: "Is this here? Is it the right shape? If not, what safe default should I use instead?"
This mirrors the nesting of the JSON itself. At the top you have a response object. Inside that is a "results" list. Inside that list you have user objects. Inside each user you have smaller objects like "name", "location", and "dob", and inside those you finally reach simple values like strings and numbers. At every hop, a .get() call (plus a few checks for list access) guards that level. The result is a multi-level defensive pattern where every access point is protected, and bad or incomplete data gets handled gracefully instead of taking down your program.
{
"results": [ ← Level 1: Array (could be empty)
{ ← Level 2: User object (could be missing)
"name": { ← Level 3: Name object (could be null)
"first": "...", ← Level 4: Actual value (could be absent)
"last": "..."
},
"location": { ← Level 3: Location object (could be null)
"street": { ← Level 4: Street object (could be null)
"number": 1234, ← Level 5: Actual value
"name": "Queen St"
},
"city": "...", ← Level 4: Actual value
"country": "..."
},
"dob": { ← Level 3: DOB object (could be null)
"age": 34 ← Level 4: Actual value
},
"email": "..." ← Level 3: Direct value
}
]
}
Each level could fail independently. The defensive pattern below protects every access point.
Safe Nested Access Pattern
import requests
# Fetch user data
response = requests.get("https://randomuser.me/api/", timeout=10)
response.raise_for_status()
# Validate content type (Chapter 4 pattern)
content_type = response.headers.get("Content-Type", "")
if "application/json" not in content_type:
print(f"Expected JSON but received {content_type}")
exit(1)
data = response.json()
# SAFE: Check each level defensively
users = data.get("results", []) # Default to empty list
if not users:
print("No users found")
exit(0)
# Get first user safely
user = users[0] # Safe now - we know list isn't empty
# Extract nested data with defaults at each level
name_obj = user.get("name", {}) # Default to empty dict
first_name = name_obj.get("first", "Unknown")
last_name = name_obj.get("last", "Unknown")
location_obj = user.get("location", {})
city = location_obj.get("city", "Unknown")
country = location_obj.get("country", "Unknown")
dob_obj = user.get("dob", {})
age = dob_obj.get("age", "Unknown")
email = user.get("email", "No email provided")
# Display safely extracted data
print(f"Name: {first_name} {last_name}")
print(f"Email: {email}")
print(f"Age: {age}")
print(f"Location: {city}, {country}")
Name: Emma Johnson
Email: alice@example.com
Age: 34
Location: Auckland, New Zealand
The Pattern at Work
Notice the defensive extraction pattern:
- Level 1:
data.get("results", []) - Get array or empty list
- Level 2: Check array not empty before accessing
[0]
- Level 3:
user.get("name", {}) - Get object or empty dict
- Level 4:
name_obj.get("first", "Unknown") - Get value or default
This pattern handles: missing keys, null values, empty arrays, and nested nulls. Every access point is protected.
The JSON Structure Being Navigated
To understand why this multi-level approach is necessary, here's the actual JSON structure returned by the API:
{
"results": [ ← Level 1: Array extracted with data.get("results", [])
{ ← Level 2: First element accessed with users[0]
"name": { ← Level 3: Object extracted with user.get("name", {})
"first": "Emma", ← Level 4: Value extracted with name_obj.get("first", "Unknown")
"last": "Johnson"
},
"email": "alice@example.com",
"dob": { ← Level 3: Object extracted with user.get("dob", {})
"date": "1991-03-15T08:23:11.Z",
"age": 34 ← Level 4: Value extracted with dob_obj.get("age", "Unknown")
},
"location": { ← Level 3: Object extracted with user.get("location", {})
"street": { ← Level 4: Street object (nested)
"number": 1234, ← Level 5: Actual value
"name": "Queen Street"
},
"city": "Auckland", ← Level 4: Value extracted with location_obj.get("city", "Unknown")
"state": "Auckland",
"country": "New Zealand", ← Level 4: Value extracted with location_obj.get("country", "Unknown")
"postcode": "1010"
}
}
],
"info": {
"seed": "abc123",
"results": 1,
"page": 1,
"version": "1.4"
}
}
This is why defensive extraction is needed at each level—any of these keys could be missing, null, or a different type than expected. Each .get() call protects against one potential failure point.
🌊 Analogy: The Stepping Stone Method
Think of accessing nested data like crossing a river on stepping stones. You are trying to get from the bank to the "Profile" stone, but you have to step on the "User" stone first.
The Problem (Bracket Notation):
If you try data["user"]["profile"], you blindly trust the "user" stone is there. If it's missing, you step into thin air and fall into the water (Crash/KeyError).
The Solution (Chained .get):
When you use data.get("user", {}), you carry a "floating dock" (the empty dictionary) with you.
- If "user" exists, you step on it normally.
- If "user" is missing, you throw down your floating dock
{} and land on that instead.
Now, when you take the next step (.get("profile")), you are standing safely on the floating dock. You look for "profile," don't find it, and safely return None—your feet stay dry, and the program doesn't crash.
This verbose approach might seem excessive for a simple user extraction, but it prevents all six failure modes from Section 2. In production, this defensive style is standard practice.
🐍 Python Idiom: The "Short-Circuit" OR
In other developers' code, you will often see this pattern:
# The "Short-Circuit" Idiom
name = user.get("name") or "Unknown"
This relies on Python's or operator. If the first value is "falsy" (None, empty string, 0, etc.), Python automatically returns the second value.
Why we didn't use it here:
This idiom is dangerous for numbers! If a user has a score of 0 (a valid number), the "Short-Circuit" would treat it as "falsy" and replace it with the default.
data.get("score", 10) → Returns 0 (Correct)
data.get("score") or 10 → Returns 10 (Bug!)
Use the idiom only when you are certain that 0 or empty strings are invalid data.
By combining the "Stepping Stone" pattern with careful defaults, you have moved beyond brittle bracket notation to production-grade extraction. While this defensive style requires more typing, it guarantees that your program stays dry and functional even when the data floodwaters rise. With this foundation in place, you are ready to encapsulate this logic into reusable functions that keep your main code clean and readable.