API data comes back as dicts inside dicts, lists inside dicts. This module is about reaching into that shape safely, and reshaping it into flat rows a report can actually use.
A spreadsheet, a CSV, a log line — they all want one flat row per record. But API data comes back nested: a user dict contains a profile dict inside it, and a groups list inside that. Before you can report on it cleanly, you often have to reshape it into flat rows.
Look at one USERS record. Name one dict-inside-a-dict in it.
profileName one list-inside-a-dict in a USERS record.
groupsIf you wanted one spreadsheet row per user, how many "email" columns would that row have — one, or one per group?
If you wanted one spreadsheet row per (user, group) MEMBERSHIP instead, would a user with 3 groups need 1 row or 3 rows?
Name one real IT/SecOps report that would need the "one row per group membership" shape instead of "one row per user."
These are the formal names for tools you already use. Serialize means turning a Python object into JSON text (json.dumps/json.dump). Deserialize means turning JSON text back into a Python object (json.loads/json.load).
import json
data = {"id": "00u001", "status": "ACTIVE"}
text = json.dumps(data) # serialize
back = json.loads(text) # deserialize
print(back == data)
Run the example above. Confirm it prints True.
TrueWhich of these four calls SERIALIZE, and which DESERIALIZE: json.dumps, json.loads, json.dump, json.load?
dumps, dump; deserialize: loads, loadSerialize a Python list of 3 numbers, then deserialize it back. Print the type at each step.
<class 'str'> then <class 'list'>Serialize a dict to a file with json.dump(), then deserialize it back with json.load(). Confirm the round-trip is equal to the original.
TrueWhy does an API send serialized (text) data instead of a live Python object directly?
Open problems/02_serialize_deserialize.py and do all 5 problems locally.
A schema is the expected shape of a payload — which keys exist, and what type each one is. Real data doesn't always match the schema you expect: a key might be missing, or a value might be None when you expected a string. Guarding with .get()/in is how you handle a schema that isn't perfectly followed.
def matches_schema(user):
has_department = "department" in user["profile"]
has_login = user.get("lastLogin") is not None
return has_department and has_login
Write matches_schema exactly as shown. Call it on USERS[7] (Layla). Print the result.
FalseCall it on USERS[3] (Dana). Print the result.
FalseCall it on USERS[0] (Julio). Print the result.
TrueLoop all 12 USERS, calling matches_schema on each. Print how many DON'T match.
2 (Dana and Layla)Write a function that prints each user's firstName and whether they matched the schema, looping all 12.
FalseOpen problems/03_schema.py and do all 5 problems locally.
Reaching into a dict-inside-a-dict or a list-inside-a-dict, beyond simple one-level lookups: pulling out a specific item from a nested list, or checking whether something specific is present in it.
def has_group(user, group_name):
names = [g["name"] for g in user["groups"]]
return group_name in names
Write has_group exactly as shown. Call it on USERS[0] (Julio) with "VPN-Users". Print the result.
TrueCall it on USERS[0] with "Admins". Print the result.
FalseCall it on USERS[3] (Dana, empty groups) with any group name. Confirm no crash — just False.
FalseWrite a function that returns just the FIRST group name a user belongs to, or None if they have none. Test it on Julio and on Dana.
Engineering then NoneWrite a function that counts how many users belong to a given group name, looping all of USERS. Call it with "VPN-Users".
5Open problems/04_nested_access.py and do all 5 problems locally.
Combine schema-checking and nested access: pull a small, safe, flat dict out of each full user record.
def extract_summary(user):
return {
"email": user["profile"]["email"],
"department": user["profile"].get("department", "Unknown"),
"group_count": len(user["groups"]),
}
Write extract_summary exactly as shown. Call it on USERS[0] (Julio). Print the result.
{'email': 'jrivera@example.com', 'department': 'IT', 'group_count': 2}Call it on USERS[7] (Layla, missing department). Confirm no crash.
department shows UnknownCall it on USERS[3] (Dana, zero groups). Print the result.
group_count is 0Loop all 12 USERS, calling extract_summary on each, building a list of the results. Print len() of that list.
12Extend extract_summary to also include "has_never_logged_in": user.get("lastLogin") is None. Run it over all 12 users, printing just the ones where that's True.
Open problems/05_extractor_checkpoint.py — 5 problems, #5 is a checkpoint combining sub-skills 2-4.
You already know list comprehensions from Module 6. This is the same tool, reaching further in — pulling values out of a NESTED structure, not just filtering a flat list.
# every group name Julio belongs to
julio_groups = [g["name"] for g in USERS[0]["groups"]]
# every group name across ALL users (flat list, duplicates allowed)
all_group_names = [g["name"] for u in USERS for g in u["groups"]]
Run the first comprehension above. Print the result.
['Engineering', 'VPN-Users']Run the second comprehension. Print len() of the result.
15Write a comprehension of every user's email who has AT LEAST ONE group.
9 emails (Dana, Tri, and Pat all have zero groups)Write a comprehension of every user's firstName who belongs to "VPN-Users" (use a helper or inline the check).
Write a comprehension building a list of (firstName, department) tuples for every user, guarding the missing department with .get().
Open problems/06_comprehensions_nested.py and do all 5 problems locally.
Fluency means going both ways: turn a loop into a comprehension, AND expand a comprehension back into a loop. Being able to read either form is what makes someone else's code (or an interview whiteboard) easy to follow.
# The loop way:
result = []
for u in USERS:
if u["status"] == "ACTIVE" and len(u["groups"]) > 1:
result.append(u["profile"]["email"])
# The comprehension way -- same result:
result = [
u["profile"]["email"]
for u in USERS
if u["status"] == "ACTIVE" and len(u["groups"]) > 1
]
Run both versions above. Confirm they produce the identical list.
['jrivera@example.com', 'achen@example.com', 'sokafor@example.com', 'mfoster@example.com'], both waysTake this comprehension and expand it into an equivalent loop:
[u["id"] for u in USERS if u["status"] == "SUSPENDED"]
Take this loop and convert it into a comprehension:
out = []
for u in USERS:
out.append(u["profile"]["lastName"])Convert this nested-loop into a single comprehension:
out = []
for u in USERS:
for g in u["groups"]:
out.append(g["name"])[g["name"] for u in USERS for g in u["groups"]], 15 itemsWrite your own comprehension with BOTH a filter and a computed expression (not just a plain field lookup) — e.g. combine two fields into one string, only for ACTIVE users.
Open problems/07_loop_comprehension_conversion.py and do all 5 problems locally.
You've built accumulator dicts before (Module 6, Module 8). Here's the same nested-loop pattern applied to flattening membership: how many users are in EACH group, across the whole fixture.
def count_group_members(users):
counts = {}
for u in users:
for g in u["groups"]:
name = g["name"]
counts[name] = counts.get(name, 0) + 1
return counts
Write count_group_members exactly as shown. Call it on USERS. Print the result.
{'Engineering': 4, 'VPN-Users': 5, 'SecOps': 2, 'Admins': 2, 'Sales-Team': 1, 'Contractors': 1}Print how many distinct groups exist (the number of keys in the result).
6Find the single group with the MOST members. Print its name and count.
VPN-Users 5Rewrite the accumulator's inner loop using a comprehension to first build the flat list of all group names, THEN accumulate over that flat list instead of the nested structure directly. Confirm the same result.
Sum all the values in the result dict. Does it equal the total number of (user, group) memberships?
15, yesOpen problems/08_accumulator_flatten.py and do all 5 problems locally.
Schema-guarding, nested access, comprehensions, and the accumulator — all together in one script.
Build the group-member-count accumulator from sub-skill 8. Print it.
Using a comprehension, build a list of every group name JULIO belongs to.
['Engineering', 'VPN-Users']Using extract_summary from sub-skill 5, build a list of summaries for every user, guarding the two bad records.
From that list of summaries, use a comprehension to pull just the emails of users with group_count == 0.
3 emails (Dana, Tri, and Pat)One script combining all four: print the accumulator, then the summaries list length, then the zero-group emails — three outputs, zero crashes.
Open problems/09_combined_checkpoint.py — 5 problems, #5 is a checkpoint combining sub-skills 3-8.
The mastery payoff: turn nested USERS records into a FLAT list of dicts, one row per (user, group) pair — the shape a report tool or spreadsheet actually wants.
def flatten_memberships(users):
rows = []
for u in users:
for g in u["groups"]:
rows.append({
"email": u["profile"]["email"],
"lastName": u["profile"]["lastName"],
"department": u["profile"].get("department", "Unknown"),
"group": g["name"],
})
return rows
Write flatten_memberships exactly as shown. Call it on USERS. Print len() of the result.
15Print the first 3 rows.
Find Layla's row(s). Confirm department shows "Unknown", not a crash.
department: 'Unknown'Confirm Dana has ZERO rows in the flattened output (she has no groups to flatten).
0 rows for DanaEach ORIGINAL user with N groups produces N rows. Verify: sum the group counts across all 12 users, confirm it equals len(flatten_memberships(USERS)).
15Rewrite flatten_memberships using a nested comprehension instead of nested loops. Confirm identical output.
Open problems/10_flatten_to_rows.py and do all 6 problems locally.
Combine flattening with Module 10's file tools: fetch the nested fixture, flatten it to rows, sort those rows, number them, and write a report to a file.
def main():
rows = flatten_memberships(USERS)
sorted_rows = sorted(rows, key=lambda r: r["lastName"])
with open("membership_report.txt", "w") as f:
for i, r in enumerate(sorted_rows, start=1):
f.write(f"{i}. {r['lastName']} -- {r['group']} ({r['department']})\n")
Write main exactly as shown (reuse your flatten_memberships). Call it.
membership_report.txt, no crashRead the report file back. Print how many lines it has.
15Print the first 3 lines of the report.
Confirm the report contains NO line for Dana (she has zero group rows).
Extend main to also print a one-line summary before writing: total rows, and how many distinct groups appear. Run the whole thing end-to-end.
15 rows, 6 groups (or your own wording)Open problems/11_final_capstone.py — 5 problems, #5 is the final checkpoint for all of Module 11.
Run python check.py in problems/ to grade everything.