Key-value mappings — how real API data actually looks. Every Okta/Slack/Google response you'll ever pull is built out of these.
A dict maps a key to a value — like a labeled lookup table. Curly braces, key: value pairs, separated by commas.
user = {"name": "Julio", "status": "ACTIVE"}
print(user)
Create a dict person with "name": "Amy", then print it.
person = {"name": "Amy"}
print(person)Create a dict scores with "math": 90 and "art": 85, then print it.
scores = {"math": 90, "art": 85}Create an empty dict called settings, then print it.
settings = {} → {}Create a dict info with "active": True and "count": 5, then print it.
Create a dict user with "id": "00u001" and "status": "ACTIVE", then print it.
Open problems/01_create.py and do all 5 problems locally.
Printing a dict shows its exact structure — braces, keys, and values, in the order you created them.
print({"a": 1}) # {'a': 1}
Print {"x": 1, "y": 2} directly.
{'x': 1, 'y': 2}Print {"active": True}.
{'active': True}Print a dict with keys in the order "z", "a", "m" — notice the print keeps that order, not alphabetical.
{'z': 1, 'a': 2, 'm': 3}Print an empty dict.
{}Print a dict with one key holding a list as its value: {"tags": ["a", "b"]}.
{'tags': ['a', 'b']}Open problems/02_print.py and do all 5 problems locally.
dict["key"] looks like list indexing, but it works differently — you give the key, not a position.
user = {"id": "00u001", "status": "ACTIVE"}
print(user["status"]) # ACTIVE
person = {"name": "Amy", "role": "Admin"}. Print person["name"].
AmyPrint person["role"].
Adminscores = {"math": 90, "art": 85}. Print scores["math"].
90Print scores["art"].
85user = {"id": "00u001", "status": "ACTIVE"}. What happens with print(user["department"]) — a key that isn't there?
KeyError: 'department'Same family as IndexError from lists — asking for something that doesn't exist. Your fixture has a user (00u008) missing the department key entirely — this is exactly that bug.
user = {"id": "00u001", "status": "ACTIVE"}. Print user["id"] — a key you know exists.
00u001Create your own 2-key dict and print one of the values by key.
Open problems/03_access_by_key.py and do all 6 problems locally.
.get()).get() asks for a key without crashing if it's missing — it just gives you None instead.
user = {"id": "00u001"}
print(user.get("department")) # None -- no crash
user = {"id": "00u002"}. Print user.get("id") — a key that exists.
00u002Print user.get("department") — a key that doesn't exist.
NoneCompare: predict what user["department"] would do (crash) vs. what you just saw with .get() (no crash).
scores = {"math": 90}. Print scores.get("art").
NonePrint bool(scores.get("art")) — combining with Module 1's truthiness.
False (None is falsy)Print scores.get("math") is None, then scores.get("art") is None — two lines.
False then TrueOpen problems/04_get.py and do all 6 problems locally.
.get(key, default) lets you choose what comes back instead of None when the key is missing.
user = {"id": "00u001"}
print(user.get("department", "Unassigned")) # Unassigned
user = {"id": "00u002"}. Print user.get("department", "Unassigned").
Unassigneduser = {"id": "00u003", "department": "Sales"}. Print user.get("department", "Unassigned") — key exists, default is ignored.
Salesscores = {"math": 90}. Print scores.get("art", 0).
0Print scores.get("math", 0) — key exists this time.
90Get a missing key with default False instead of a string, print it.
FalseOne script: create a dict, print it, access a key directly, use .get() on a missing key, and use .get() with a default on the same missing key. Four lines of output.
Open problems/05_get_default.py — 6 problems, #6 is a checkpoint combining sub-skills 1–5.
len() — same function as always — counts the number of key-value pairs.
user = {"id": "00u001", "status": "ACTIVE"}
print(len(user)) # 2
Print len({"a": 1, "b": 2, "c": 3}).
3Print len({}).
0user = {"id": "00u001"}. Add nothing yet — just print len(user).
1Print len({"x": 1}) vs len(["x"]) — a 1-key dict and a 1-item list, same length.
1 then 1Print len({"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}).
5Open problems/06_length.py and do all 5 problems locally.
in)in on a dict checks the keys, not the values — even if a value matches, in won't find it unless it's a key.
user = {"status": "ACTIVE"}
print("status" in user) # True -- "status" is a key
print("ACTIVE" in user) # False -- "ACTIVE" is a value, not a key
user = {"id": "00u001", "status": "ACTIVE"}. Print "status" in user.
TruePrint "department" in user.
FalsePrint "ACTIVE" in user — a value, not a key.
FalsePrint "id" not in user.
FalsePrint "x" in {} — an empty dict.
FalseUse in to decide what .get() would have told you: print "status" in user, then print user.get("status") — two lines.
True then ACTIVEuser = {"id": "00u001", "status": "ACTIVE"}. Is "00u001" in user — checking a value — True or False?
False"00u001" is a value here, not a key — in only ever checks keys on a dict.
Open problems/07_membership.py and do all 7 problems locally.
dict[key] = value creates the key if it doesn't exist yet.
user = {"id": "00u001"}
user["status"] = "ACTIVE"
print(user) # {'id': '00u001', 'status': 'ACTIVE'}
person = {"name": "Amy"}. Add "role": "Admin", print person.
{'name': 'Amy', 'role': 'Admin'}Add a second new key "active": True too, print again.
settings = {}. Add "theme": "dark", print settings.
{'theme': 'dark'}After adding a key, print len() to confirm it grew.
Add a key, then confirm with in that it's there.
TrueAdd a key, then read it back with .get() to confirm the value.
Start with an empty dict, add three different keys one at a time, print the final dict.
Open problems/08_add.py and do all 7 problems locally.
Same syntax as adding — dict[key] = value — but this time the key already exists, so it overwrites instead of creating.
user = {"status": "ACTIVE"}
user["status"] = "SUSPENDED"
print(user) # {'status': 'SUSPENDED'}
user = {"status": "ACTIVE"}. Update "status" to "SUSPENDED", print user.
{'status': 'SUSPENDED'}scores = {"math": 90}. Update "math" to 95, print scores.
{'math': 95}Update a key, then confirm len() stayed the same (updating doesn't add a key).
user = {"status": "ACTIVE"}. Print the value before and after updating "status" to "DEPROVISIONED" — two lines.
ACTIVE then DEPROVISIONEDUpdate a key to hold a completely different type than before (string → number), print the result.
Update the same key twice in a row, print only the final value.
Simulate a deprovision: user = {"status": "ACTIVE"}, update to "SUSPENDED", then update again to "DEPROVISIONED". Print the final dict.
Open problems/09_update.py and do all 7 problems locally.
.pop())dict.pop(key) removes a key and hands back its value. This is the same method name as list.pop(), but it takes a key here, not a position — same word, different job.
user = {"id": "00u001", "status": "ACTIVE"}
removed = user.pop("status")
print(removed) # ACTIVE
print(user) # {'id': '00u001'}
user = {"id": "00u001", "status": "ACTIVE"}. Pop "status" into a variable, print the variable.
ACTIVEAfter popping, print user — confirm the key is gone.
{'id': '00u001'}Pop a key, then print len() to confirm it shrank.
Pop a key, then confirm with in that it's gone.
Falsed = {"a": 1}. Pop "a" with a default: d.pop("b", "not found") — key that doesn't exist, but a default is given.
not found, no crashd = {"a": 1}. What happens with d.pop("b") — no default this time?
KeyError: 'b'Just like accessing a missing key with [] — without a default, .pop() on a missing key crashes too.
Compare: colors = ["red", "blue"], pop position 0, print what came back. user = {"a": 1}, pop key "a", print what came back. Same method name, different kind of argument.
red then 1Add a key, then immediately pop it back off, print the dict before and after (should match).
One script: create a dict with 2 keys, add a third key, update one of the original two, pop the third key back off, print what was popped and the final dict.
Open problems/10_pop.py — 8 problems, #8 is a checkpoint combining sub-skills 6–10.
Same truthiness rule from Module 1: an empty dict is falsy. bool(my_dict) tells you if it has anything in it.
settings = {}
print(bool(settings)) # False -- empty
Print bool({}).
FalsePrint bool({"a": 1}) — one key.
Trued = {"a": 1}. Pop the only key, then print bool(d).
FalseCompare len(d) == 0 to bool(d) on the same empty dict.
True and False, both correctly say "empty"Start with an empty dict, add a key, print bool() before and after adding.
False then TrueCreate a dict with 1 key, pop it by name, print bool() of the result.
FalseOpen problems/11_empty_check.py and do all 6 problems locally.
A value can be another dict. Chain the brackets to reach in: user["profile"]["email"] means "get profile, then get email from that."
user = {"id": "00u001", "profile": {"email": "jrivera@example.com", "department": "IT"}}
print(user["profile"]["email"]) # jrivera@example.com
user = {"profile": {"email": "achen@example.com"}}. Print user["profile"]["email"].
achen@example.comPrint user["profile"] alone first — see it's a whole dict, not a string.
{'email': 'achen@example.com'}user = {"profile": {"department": "Security"}}. Print user["profile"]["department"].
SecurityUse .get() on the outer dict, then index the result: user.get("profile")["department"].
SecurityStore profile = user["profile"] in its own variable first, then print profile["department"] — same result, split into two steps.
Predict: your fixture's 00u008 has no department key inside its profile. What would user["profile"]["department"] do for that record?
KeyError: 'department'Safer version of the above: use .get() on the inner dict instead — user["profile"].get("department", "Unknown").
Build your own 2-level nested dict from scratch and print one deeply-nested value.
Open problems/12_nested_dict.py and do all 8 problems locally.
A value can be a list too. Get the list with a key, then index into it like any list.
user = {"id": "00u001", "groups": ["Engineering", "VPN-Users"]}
print(user["groups"][0]) # Engineering
user = {"groups": ["Sales", "IT"]}. Print user["groups"][0].
SalesPrint user["groups"][-1] — the last group.
ITPrint len(user["groups"]) — how many groups.
2Print "IT" in user["groups"].
Trueuser = {"groups": []}. Print bool(user["groups"]) — like a user with zero groups in your fixture.
FalseStore groups = user["groups"] first, then .append() a new group to it, print groups.
Print a slice of the groups list: user["groups"][0:1].
Build your own dict with a 3-item list value, print the middle item by index.
Open problems/13_nested_list.py and do all 8 problems locally.
.update()).update() merges another dict's keys and values in. Matching keys get overwritten; new keys just get added.
a = {"id": "00u001", "status": "ACTIVE"}
b = {"status": "SUSPENDED", "department": "IT"}
a.update(b)
print(a) # {'id': '00u001', 'status': 'SUSPENDED', 'department': 'IT'}
a = {"x": 1}, b = {"y": 2}. Update a with b, print a.
{'x': 1, 'y': 2}a = {"x": 1}, b = {"x": 99}. Update a with b, print a — matching key gets overwritten.
{'x': 99}Update a dict with an empty dict, print the result — nothing changes.
Merge, then print len() of the result.
Merge, then check in for a key that only existed in the second dict.
TrueBuild a dict via [key] = assignment (Module 3's "add"), a second dict as a literal, merge them.
Merge two dicts, then .get() a key from the result with a default.
Merge, then pop one of the merged-in keys back off, print the final dict.
Merge a dict containing a nested dict value with another dict, print the result and access the nested value.
One script using every sub-skill 1–14 at least once: create, print, access by key, .get(), .get() with default, length, membership, add, update, pop, empty-check, nested dict, nested list, and merge — ending in one printed summary line.
Open problems/14_merge.py — 10 problems, #10 is the final checkpoint for all of Module 3.
Run python check.py in problems/ to grade everything.