1. Model one game world with several collection jobs
Real data rarely fits one flat collection. A game world may preserve region order, name each region’s fields, keep one fixed coordinate, group unique hazards, and store a nested weather record:
world = [
{
"id" : "garden" ,
"coordinate" : (4 , 7 ),
"hazards" : {"fog" },
"weather" : {"condition" : "mist" , "temperature" : 12 },
},
{
"id" : "vault" ,
"coordinate" : (9 , 2 ),
"hazards" : {"lock" , "darkness" },
"weather" : {"condition" : "dry" , "temperature" : 18 },
},
]
Read its type shape from the outside inward:
list
└── dictionary region record
├── "id" -> string
├── "coordinate" -> tuple of two integers
├── "hazards" -> set of strings
└── "weather" -> dictionary
├── "condition" -> string
└── "temperature" -> integer
No single container is doing every job. The outer list preserves region display order. Each dictionary names fields. The tuple preserves a fixed coordinate, the set preserves unique hazards, and the nested dictionary names weather fields.
This lesson answers:
How can you read a long path without guessing which type comes next?
What do TypeError, KeyError, and IndexError reveal about the failed level?
How should absent, stored-as-None, and empty values remain distinct?
When should an ordered list of records gain a separate direct-lookup dictionary?
2. Navigate one container level at a time
The compact access below reaches the vault temperature:
temperature = world[1 ]["weather" ]["temperature" ]
print (temperature)
Read every operation against its immediate left-hand value:
world[1] indexes the outer list and produces a region dictionary;
...["weather"] looks up a key and produces a weather dictionary; and
...["temperature"] looks up a key and produces integer 18.
When the path is unfamiliar, store the intermediate levels:
vault_record = world[1 ]
vault_weather = vault_record["weather" ]
vault_temperature = vault_weather["temperature" ]
print (type (vault_record).__name__ )
print (type (vault_weather).__name__ )
print (type (vault_temperature).__name__ )
Intermediate names turn an opaque chain into inspectable evidence. They also give tracebacks a smaller region of code to identify.
Each access operation selects one level and produces the container or scalar needed by the next operation.
flowchart LR
A["world list"] -->|"index 1"| B["vault dictionary"]
B -->|"weather key"| C["weather dictionary"]
C -->|"temperature key"| D["integer 18"]
Draw the path before writing it
For each requested result, state the levels:
First region ID
world[0]["id"]
list → dictionary → string
Vault row
world[1]["coordinate"][0]
list → dictionary → tuple → integer
Garden hazards
world[0]["hazards"]
list → dictionary → set
Garden condition
world[0]["weather"]["condition"]
list → dictionary → dictionary → string
An index belongs to a sequence. A key belongs to a mapping. Membership belongs to a set-like group. Choosing the next operation depends on the value produced at the current level.
3. Wrong operations produce different evidence
Applying a string key to the outer list raises TypeError:
The outer value is a list and expects an integer or slice. The error does not mean the key "vault" is misspelled; it means this level has the wrong operation .
A missing dictionary field raises KeyError:
terrain = world[0 ]["terrain" ]
The region record is a dictionary, but it contains no "terrain" key.
An absent sequence position raises IndexError:
The outer list has only positions 0 and 1.
A set rejects numeric indexing with TypeError:
first_hazard = world[1 ]["hazards" ][0 ]
The path reaches the right field, but a set has no positional member zero.
TypeError often means the current value does not support the attempted kind of access.
KeyError means mapping access reached a dictionary but the requested key is absent.
IndexError means sequence access reached a sequence but the position is absent.
Print or inspect the value immediately to the left of the failing brackets before changing the full chain.
Checkpoint: reading nested paths
{
const scripts = Array . from (
document . querySelectorAll ("script.fcpython-ojs-quiz-config" )
);
const script = scripts. find (
(node) => node. dataset . fcpythonRendered !== "true"
);
if (! script) {
return html `<div class="fcpython-quiz fcpython-quiz-warning">
Quiz configuration was not found.
</div>` ;
}
script. dataset . fcpythonRendered = "true" ;
const quiz = JSON . parse (script. textContent );
const container = html `<div class="fcpython-quiz"></div>` ;
const title = document . createElement ("h3" );
title. textContent = quiz. title ;
container. appendChild (title);
const instructions = document . createElement ("p" );
instructions. textContent = quiz. instructions ;
container. appendChild (instructions);
const progress = document . createElement ("div" );
progress. className = "fcpython-quiz-progress" ;
const counter = document . createElement ("p" );
counter. className = "fcpython-quiz-counter" ;
counter. setAttribute ("aria-live" , "polite" );
progress. appendChild (counter);
const tabs = document . createElement ("div" );
tabs. className = "fcpython-quiz-steps" ;
tabs. setAttribute ("role" , "tablist" );
tabs. setAttribute ("aria-label" , "Quiz questions" );
progress. appendChild (tabs);
container. appendChild (progress);
const questions = document . createElement ("div" );
questions. className = "fcpython-quiz-questions" ;
const feedbackNodes = [];
const questionPanels = [];
const stepButtons = [];
let currentQuestion = 0 ;
function setStepStatus (questionIndex, status) {
const step = stepButtons[questionIndex];
const question = quiz. questions [questionIndex];
const statuses = {
answered : "answered" ,
correct : "correct" ,
incorrect : "incorrect" ,
unanswered : "not answered" ,
};
step. classList . remove (
"is-answered" ,
"is-correct" ,
"is-incorrect" ,
"is-unanswered"
);
if (status) {
step. classList . add (`is- ${ status} ` );
}
const statusLabel = status ? `, ${ statuses[status]} ` : "" ;
step. setAttribute (
"aria-label" ,
`Question ${ questionIndex + 1 } : ${ question. prompt }${ statusLabel} `
);
}
function showQuestion (questionIndex, focusPanel = false ) {
currentQuestion = Math . max (
0 ,
Math . min (questionIndex, quiz. questions . length - 1 )
);
questionPanels. forEach ((panel, index) => {
panel. hidden = index !== currentQuestion;
});
stepButtons. forEach ((step, index) => {
const isCurrent = index === currentQuestion;
step. classList . toggle ("is-current" , isCurrent);
step. setAttribute ("aria-selected" , String (isCurrent));
step. tabIndex = isCurrent ? 0 : - 1 ;
});
counter. textContent = `Question ${ currentQuestion + 1 } of ${ quiz. questions . length } ` ;
const selected = questionPanels[currentQuestion]. querySelector (
"input[type='radio']:checked"
);
const isLastQuestion = currentQuestion === quiz. questions . length - 1 ;
previous. hidden = currentQuestion === 0 ;
next. hidden = isLastQuestion || ! selected;
check. hidden = ! isLastQuestion;
if (focusPanel) {
questionPanels[currentQuestion]. focus ();
}
}
quiz. questions . forEach ((question, questionIndex) => {
const tabId = ` ${ quiz. id } -question-tab- ${ questionIndex + 1 } ` ;
const panelId = ` ${ quiz. id } -question-panel- ${ questionIndex + 1 } ` ;
const step = document . createElement ("button" );
step. type = "button" ;
step. className = "fcpython-quiz-step" ;
step. id = tabId;
step. textContent = String (questionIndex + 1 );
step. setAttribute ("role" , "tab" );
step. setAttribute ("aria-controls" , panelId);
step. setAttribute ("aria-selected" , "false" );
step. tabIndex = - 1 ;
step. addEventListener ("click" , () => showQuestion (questionIndex));
step. addEventListener ("keydown" , (event ) => {
let destination = null ;
if (event . key === "ArrowRight" ) {
destination = (questionIndex + 1 ) % quiz. questions . length ;
} else if (event . key === "ArrowLeft" ) {
destination =
(questionIndex - 1 + quiz. questions . length ) % quiz. questions . length ;
} else if (event . key === "Home" ) {
destination = 0 ;
} else if (event . key === "End" ) {
destination = quiz. questions . length - 1 ;
}
if (destination !== null ) {
event . preventDefault ();
showQuestion (destination);
stepButtons[destination]. focus ();
}
});
stepButtons. push (step);
tabs. appendChild (step);
setStepStatus (questionIndex, "" );
const panel = document . createElement ("div" );
panel. className = "fcpython-quiz-panel" ;
panel. id = panelId;
panel. setAttribute ("role" , "tabpanel" );
panel. setAttribute ("aria-labelledby" , tabId);
panel. tabIndex = - 1 ;
const fieldset = document . createElement ("fieldset" );
fieldset. className = "fcpython-quiz-question" ;
const legend = document . createElement ("legend" );
legend. textContent = question. prompt ;
fieldset. appendChild (legend);
question. options . forEach ((option, optionIndex) => {
const label = document . createElement ("label" );
label. className = "fcpython-quiz-option" ;
const input = document . createElement ("input" );
input. type = "radio" ;
input. name = ` ${ quiz. id } - ${ question. id } ` ;
input. value = String (optionIndex);
input. addEventListener ("change" , () => {
setStepStatus (questionIndex, "answered" );
feedbackNodes[questionIndex]. textContent = "" ;
feedbackNodes[questionIndex]. className = "fcpython-quiz-feedback" ;
score. textContent = "" ;
showQuestion (questionIndex);
});
const text = document . createElement ("span" );
text. textContent = option;
label. appendChild (input);
label. appendChild (text);
fieldset. appendChild (label);
});
const feedback = document . createElement ("p" );
feedback. className = "fcpython-quiz-feedback" ;
feedback. setAttribute ("aria-live" , "polite" );
feedbackNodes. push (feedback);
fieldset. appendChild (feedback);
panel. appendChild (fieldset);
questionPanels. push (panel);
questions. appendChild (panel);
});
container. appendChild (questions);
const actions = document . createElement ("div" );
actions. className = "fcpython-quiz-actions" ;
const previous = document . createElement ("button" );
previous. type = "button" ;
previous. className = "fcpython-quiz-secondary" ;
previous. textContent = "Previous" ;
previous. addEventListener ("click" , () => {
showQuestion (currentQuestion - 1 , true );
});
const next = document . createElement ("button" );
next. type = "button" ;
next. textContent = "Next question" ;
next. addEventListener ("click" , () => {
showQuestion (currentQuestion + 1 , true );
});
const check = document . createElement ("button" );
check. type = "button" ;
check. textContent = "Check answers" ;
const reset = document . createElement ("button" );
reset. type = "button" ;
reset. className = "fcpython-quiz-secondary fcpython-quiz-reset" ;
reset. textContent = "Reset" ;
const score = document . createElement ("p" );
score. className = "fcpython-quiz-score" ;
score. setAttribute ("aria-live" , "polite" );
check. addEventListener ("click" , () => {
let correctCount = 0 ;
let firstQuestionToReview = null ;
quiz. questions . forEach ((question, questionIndex) => {
const selected = container. querySelector (
`input[name=" ${ quiz. id } - ${ question. id } "]:checked`
);
const feedback = feedbackNodes[questionIndex];
if (! selected) {
feedback. textContent = "Choose an answer before checking." ;
feedback. className = "fcpython-quiz-feedback" ;
setStepStatus (questionIndex, "unanswered" );
if (firstQuestionToReview === null ) {
firstQuestionToReview = questionIndex;
}
return ;
}
const selectedIndex = Number (selected. value );
if (selectedIndex === question. answer_index ) {
correctCount += 1 ;
feedback. textContent = `✅ Correct. ${ question. explanation } ` ;
feedback. className = "fcpython-quiz-feedback is-correct" ;
setStepStatus (questionIndex, "correct" );
} else {
const answer = question. options [question. answer_index ];
feedback. textContent = `❌ Not yet. Correct answer: ${ answer} . ${ question. explanation } ` ;
feedback. className = "fcpython-quiz-feedback is-incorrect" ;
setStepStatus (questionIndex, "incorrect" );
if (firstQuestionToReview === null ) {
firstQuestionToReview = questionIndex;
}
}
});
score. textContent = `Score: ${ correctCount} / ${ quiz. questions . length } ` ;
if (firstQuestionToReview !== null ) {
showQuestion (firstQuestionToReview, true );
}
});
reset. addEventListener ("click" , () => {
container. querySelectorAll ("input[type='radio']" ). forEach ((input) => {
input. checked = false ;
});
feedbackNodes. forEach ((feedback) => {
feedback. textContent = "" ;
feedback. className = "fcpython-quiz-feedback" ;
});
stepButtons. forEach ((_, questionIndex) => {
setStepStatus (questionIndex, "" );
});
score. textContent = "" ;
showQuestion (0 , true );
});
actions. appendChild (previous);
actions. appendChild (next);
actions. appendChild (check);
actions. appendChild (reset);
container. appendChild (actions);
container. appendChild (score);
showQuestion (0 );
return container;
}
4. Choose a nested shape from the dominant question
Several shapes can store the same facts, but they optimize different access questions.
A list of dictionaries preserves record order
regions = [
{"id" : "garden" , "temperature" : 12 },
{"id" : "vault" , "temperature" : 18 },
]
Use this when ordered display or repeated full-record traversal matters. Finding one ID requires a search unless another index is built.
A dictionary of dictionaries gives direct ID lookup
regions_by_id = {
"garden" : {"temperature" : 12 },
"vault" : {"temperature" : 18 },
}
print (regions_by_id["vault" ]["temperature" ])
Use this when stable IDs are unique and direct lookup dominates. The ID can live only as the outer key or be repeated inside each record when complete standalone records are valuable; choose one consistent contract.
A dictionary of lists groups values
regions_by_climate = {
"cool" : ["garden" , "tower" ],
"warm" : ["vault" ],
}
Use this when the central question is “Which region IDs belong to this group?” The list retains group order and can contain several members.
A list of lists models a position-based grid
grid = [
["dock" , "water" , "water" ],
["path" , "garden" , "wall" ],
["path" , "tower" , "vault" ],
]
print (grid[2 ][1 ])
The result is "tower": choose row 2 from the outer list, then column 1 from that row. A rectangular grid is a good positional shape only when rows and columns have consistent meaning.
No shape wins universally. Preserve more than one view when the program has both ordered-display and repeated-direct-lookup requirements, and define which view is the source of truth.
5. Update the exact nested container you intend
Assignment through a complete path mutates the reached dictionary:
world = [
{
"id" : "garden" ,
"weather" : {"condition" : "mist" , "temperature" : 12 },
},
{
"id" : "vault" ,
"weather" : {"condition" : "dry" , "temperature" : 18 },
},
]
world[0 ]["weather" ]["temperature" ] = 13
assert world[0 ]["weather" ]["temperature" ] == 13
The outer list still has two positions. The first region dictionary still has the same fields. The nested weather dictionary’s temperature value changed.
Intermediate names can make the mutation target clearer:
garden = world[0 ]
garden_weather = garden["weather" ]
garden_weather["condition" ] = "clear"
assert world[0 ]["weather" ]["condition" ] == "clear"
garden_weather is an alias for the nested dictionary; it is not a detached copy.
Outer copies still share nested values
source_record = {
"id" : "garden" ,
"weather" : {"condition" : "mist" , "temperature" : 12 },
}
working_record = source_record.copy()
working_record["weather" ]["temperature" ] = 13
print (source_record["weather" ]["temperature" ])
The source also reports 13 because .copy() separated only the outer dictionary. For this one known shape, reconstruct the nested level deliberately:
source_record = {
"id" : "garden" ,
"weather" : {"condition" : "mist" , "temperature" : 12 },
}
working_record = source_record.copy()
working_record["weather" ] = source_record["weather" ].copy()
working_record["weather" ]["temperature" ] = 13
assert source_record["weather" ]["temperature" ] == 12
assert working_record["weather" ]["temperature" ] == 13
This is not a universal deep-copy recipe. Unit 6 teaches shared reference graphs, copy.deepcopy(), and when rebuilding a value is preferable.
6. Keep absent, None, and empty values distinct
Consider three records:
records = [
{"id" : "garden" },
{"id" : "vault" , "guide" : None },
{"id" : "tower" , "guide" : "Nova" , "hazards" : []},
]
garden has no "guide" field;
vault has a guide field whose value is deliberately missing; and
tower has a guide name and an empty hazards list.
Membership and .get() serve different questions:
garden, vault, tower = records
assert "guide" not in garden
assert "guide" in vault and vault["guide" ] is None
assert tower.get("guide" ) == "Nova"
assert "hazards" in tower and tower["hazards" ] == []
.get("guide") alone returns None for both the absent garden field and the stored-None vault field. Check membership when the schema state matters.
A default should match the expected type:
garden_hazards = garden.get("hazards" , [])
assert garden_hazards == []
Do not mutate a shared default object stored elsewhere. The fresh literal above is used only as a read fallback.
7. Consistent record shapes make traversal predictable
This collection promises that each record has an ID and coordinate:
regions = [
{"id" : "garden" , "coordinate" : (4 , 7 )},
{"id" : "vault" , "coordinate" : (9 , 2 )},
]
for region in regions:
region_id = region["id" ]
row, column = region["coordinate" ]
print (region_id, row, column)
A malformed record exposes the broken promise:
regions = [
{"id" : "garden" , "coordinate" : (4 , 7 )},
{"name" : "vault" , "coordinate" : (9 ,)},
]
for region in regions:
region_id = region["id" ]
row, column = region["coordinate" ]
print (region_id, row, column)
The second record first raises KeyError for "id"; after that key is repaired, its one-item coordinate raises an unpacking ValueError. These are separate schema failures, and both are more useful than guessing around them with unrelated defaults.
External validation belongs in Unit 9. Inside this lesson, write and assert the shape that your own in-memory examples promise.
Checkpoint: shape failures and optional values
{
const scripts = Array . from (
document . querySelectorAll ("script.fcpython-ojs-quiz-config" )
);
const script = scripts. find (
(node) => node. dataset . fcpythonRendered !== "true"
);
if (! script) {
return html `<div class="fcpython-quiz fcpython-quiz-warning">
Quiz configuration was not found.
</div>` ;
}
script. dataset . fcpythonRendered = "true" ;
const quiz = JSON . parse (script. textContent );
const container = html `<div class="fcpython-quiz"></div>` ;
const title = document . createElement ("h3" );
title. textContent = quiz. title ;
container. appendChild (title);
const instructions = document . createElement ("p" );
instructions. textContent = quiz. instructions ;
container. appendChild (instructions);
const progress = document . createElement ("div" );
progress. className = "fcpython-quiz-progress" ;
const counter = document . createElement ("p" );
counter. className = "fcpython-quiz-counter" ;
counter. setAttribute ("aria-live" , "polite" );
progress. appendChild (counter);
const tabs = document . createElement ("div" );
tabs. className = "fcpython-quiz-steps" ;
tabs. setAttribute ("role" , "tablist" );
tabs. setAttribute ("aria-label" , "Quiz questions" );
progress. appendChild (tabs);
container. appendChild (progress);
const questions = document . createElement ("div" );
questions. className = "fcpython-quiz-questions" ;
const feedbackNodes = [];
const questionPanels = [];
const stepButtons = [];
let currentQuestion = 0 ;
function setStepStatus (questionIndex, status) {
const step = stepButtons[questionIndex];
const question = quiz. questions [questionIndex];
const statuses = {
answered : "answered" ,
correct : "correct" ,
incorrect : "incorrect" ,
unanswered : "not answered" ,
};
step. classList . remove (
"is-answered" ,
"is-correct" ,
"is-incorrect" ,
"is-unanswered"
);
if (status) {
step. classList . add (`is- ${ status} ` );
}
const statusLabel = status ? `, ${ statuses[status]} ` : "" ;
step. setAttribute (
"aria-label" ,
`Question ${ questionIndex + 1 } : ${ question. prompt }${ statusLabel} `
);
}
function showQuestion (questionIndex, focusPanel = false ) {
currentQuestion = Math . max (
0 ,
Math . min (questionIndex, quiz. questions . length - 1 )
);
questionPanels. forEach ((panel, index) => {
panel. hidden = index !== currentQuestion;
});
stepButtons. forEach ((step, index) => {
const isCurrent = index === currentQuestion;
step. classList . toggle ("is-current" , isCurrent);
step. setAttribute ("aria-selected" , String (isCurrent));
step. tabIndex = isCurrent ? 0 : - 1 ;
});
counter. textContent = `Question ${ currentQuestion + 1 } of ${ quiz. questions . length } ` ;
const selected = questionPanels[currentQuestion]. querySelector (
"input[type='radio']:checked"
);
const isLastQuestion = currentQuestion === quiz. questions . length - 1 ;
previous. hidden = currentQuestion === 0 ;
next. hidden = isLastQuestion || ! selected;
check. hidden = ! isLastQuestion;
if (focusPanel) {
questionPanels[currentQuestion]. focus ();
}
}
quiz. questions . forEach ((question, questionIndex) => {
const tabId = ` ${ quiz. id } -question-tab- ${ questionIndex + 1 } ` ;
const panelId = ` ${ quiz. id } -question-panel- ${ questionIndex + 1 } ` ;
const step = document . createElement ("button" );
step. type = "button" ;
step. className = "fcpython-quiz-step" ;
step. id = tabId;
step. textContent = String (questionIndex + 1 );
step. setAttribute ("role" , "tab" );
step. setAttribute ("aria-controls" , panelId);
step. setAttribute ("aria-selected" , "false" );
step. tabIndex = - 1 ;
step. addEventListener ("click" , () => showQuestion (questionIndex));
step. addEventListener ("keydown" , (event ) => {
let destination = null ;
if (event . key === "ArrowRight" ) {
destination = (questionIndex + 1 ) % quiz. questions . length ;
} else if (event . key === "ArrowLeft" ) {
destination =
(questionIndex - 1 + quiz. questions . length ) % quiz. questions . length ;
} else if (event . key === "Home" ) {
destination = 0 ;
} else if (event . key === "End" ) {
destination = quiz. questions . length - 1 ;
}
if (destination !== null ) {
event . preventDefault ();
showQuestion (destination);
stepButtons[destination]. focus ();
}
});
stepButtons. push (step);
tabs. appendChild (step);
setStepStatus (questionIndex, "" );
const panel = document . createElement ("div" );
panel. className = "fcpython-quiz-panel" ;
panel. id = panelId;
panel. setAttribute ("role" , "tabpanel" );
panel. setAttribute ("aria-labelledby" , tabId);
panel. tabIndex = - 1 ;
const fieldset = document . createElement ("fieldset" );
fieldset. className = "fcpython-quiz-question" ;
const legend = document . createElement ("legend" );
legend. textContent = question. prompt ;
fieldset. appendChild (legend);
question. options . forEach ((option, optionIndex) => {
const label = document . createElement ("label" );
label. className = "fcpython-quiz-option" ;
const input = document . createElement ("input" );
input. type = "radio" ;
input. name = ` ${ quiz. id } - ${ question. id } ` ;
input. value = String (optionIndex);
input. addEventListener ("change" , () => {
setStepStatus (questionIndex, "answered" );
feedbackNodes[questionIndex]. textContent = "" ;
feedbackNodes[questionIndex]. className = "fcpython-quiz-feedback" ;
score. textContent = "" ;
showQuestion (questionIndex);
});
const text = document . createElement ("span" );
text. textContent = option;
label. appendChild (input);
label. appendChild (text);
fieldset. appendChild (label);
});
const feedback = document . createElement ("p" );
feedback. className = "fcpython-quiz-feedback" ;
feedback. setAttribute ("aria-live" , "polite" );
feedbackNodes. push (feedback);
fieldset. appendChild (feedback);
panel. appendChild (fieldset);
questionPanels. push (panel);
questions. appendChild (panel);
});
container. appendChild (questions);
const actions = document . createElement ("div" );
actions. className = "fcpython-quiz-actions" ;
const previous = document . createElement ("button" );
previous. type = "button" ;
previous. className = "fcpython-quiz-secondary" ;
previous. textContent = "Previous" ;
previous. addEventListener ("click" , () => {
showQuestion (currentQuestion - 1 , true );
});
const next = document . createElement ("button" );
next. type = "button" ;
next. textContent = "Next question" ;
next. addEventListener ("click" , () => {
showQuestion (currentQuestion + 1 , true );
});
const check = document . createElement ("button" );
check. type = "button" ;
check. textContent = "Check answers" ;
const reset = document . createElement ("button" );
reset. type = "button" ;
reset. className = "fcpython-quiz-secondary fcpython-quiz-reset" ;
reset. textContent = "Reset" ;
const score = document . createElement ("p" );
score. className = "fcpython-quiz-score" ;
score. setAttribute ("aria-live" , "polite" );
check. addEventListener ("click" , () => {
let correctCount = 0 ;
let firstQuestionToReview = null ;
quiz. questions . forEach ((question, questionIndex) => {
const selected = container. querySelector (
`input[name=" ${ quiz. id } - ${ question. id } "]:checked`
);
const feedback = feedbackNodes[questionIndex];
if (! selected) {
feedback. textContent = "Choose an answer before checking." ;
feedback. className = "fcpython-quiz-feedback" ;
setStepStatus (questionIndex, "unanswered" );
if (firstQuestionToReview === null ) {
firstQuestionToReview = questionIndex;
}
return ;
}
const selectedIndex = Number (selected. value );
if (selectedIndex === question. answer_index ) {
correctCount += 1 ;
feedback. textContent = `✅ Correct. ${ question. explanation } ` ;
feedback. className = "fcpython-quiz-feedback is-correct" ;
setStepStatus (questionIndex, "correct" );
} else {
const answer = question. options [question. answer_index ];
feedback. textContent = `❌ Not yet. Correct answer: ${ answer} . ${ question. explanation } ` ;
feedback. className = "fcpython-quiz-feedback is-incorrect" ;
setStepStatus (questionIndex, "incorrect" );
if (firstQuestionToReview === null ) {
firstQuestionToReview = questionIndex;
}
}
});
score. textContent = `Score: ${ correctCount} / ${ quiz. questions . length } ` ;
if (firstQuestionToReview !== null ) {
showQuestion (firstQuestionToReview, true );
}
});
reset. addEventListener ("click" , () => {
container. querySelectorAll ("input[type='radio']" ). forEach ((input) => {
input. checked = false ;
});
feedbackNodes. forEach ((feedback) => {
feedback. textContent = "" ;
feedback. className = "fcpython-quiz-feedback" ;
});
stepButtons. forEach ((_, questionIndex) => {
setStepStatus (questionIndex, "" );
});
score. textContent = "" ;
showQuestion (0 , true );
});
actions. appendChild (previous);
actions. appendChild (next);
actions. appendChild (check);
actions. appendChild (reset);
container. appendChild (actions);
container. appendChild (score);
showQuestion (0 );
return container;
}
9. Add a lookup view without discarding source order
An ordered source and keyed index can coexist:
regions = [
{"id" : "garden" , "temperature" : 12 },
{"id" : "vault" , "temperature" : 18 },
{"id" : "tower" , "temperature" : 9 },
]
regions_by_id = {}
for region in regions:
regions_by_id[region["id" ]] = region
assert regions_by_id["vault" ]["temperature" ] == 18
assert regions[0 ]["id" ] == "garden"
The list remains the ordered source. The dictionary is a secondary index for direct lookup. Its values intentionally reference the same record objects. If the program permits mutations, document which view owns changes so the structures do not drift.
Group IDs for another question:
regions = [
{"id" : "garden" , "climate" : "cool" },
{"id" : "vault" , "climate" : "warm" },
{"id" : "tower" , "climate" : "cool" },
]
ids_by_climate = {}
for region in regions:
climate = region["climate" ]
if climate not in ids_by_climate:
ids_by_climate[climate] = []
ids_by_climate[climate].append(region["id" ])
assert ids_by_climate == {
"cool" : ["garden" , "tower" ],
"warm" : ["vault" ],
}
One source can support several derived views, each answering a named question.
10. JSON-like does not mean JSON has been parsed
Lists, dictionaries, strings, numbers, Booleans, and None resemble the values commonly produced by parsing JSON:
json_like_value = {
"regions" : [
{"id" : "garden" , "open" : True },
{"id" : "vault" , "open" : False },
],
"next_page" : None ,
}
This is an ordinary in-memory Python value. No file was opened and no external text was parsed. Unit 9 teaches JSON parsing, file encodings, schemas, and validation at an external-data boundary. The collection skills here prepare you to inspect the resulting shape safely.
Checkpoint: choosing and refactoring shapes
{
const scripts = Array . from (
document . querySelectorAll ("script.fcpython-ojs-quiz-config" )
);
const script = scripts. find (
(node) => node. dataset . fcpythonRendered !== "true"
);
if (! script) {
return html `<div class="fcpython-quiz fcpython-quiz-warning">
Quiz configuration was not found.
</div>` ;
}
script. dataset . fcpythonRendered = "true" ;
const quiz = JSON . parse (script. textContent );
const container = html `<div class="fcpython-quiz"></div>` ;
const title = document . createElement ("h3" );
title. textContent = quiz. title ;
container. appendChild (title);
const instructions = document . createElement ("p" );
instructions. textContent = quiz. instructions ;
container. appendChild (instructions);
const progress = document . createElement ("div" );
progress. className = "fcpython-quiz-progress" ;
const counter = document . createElement ("p" );
counter. className = "fcpython-quiz-counter" ;
counter. setAttribute ("aria-live" , "polite" );
progress. appendChild (counter);
const tabs = document . createElement ("div" );
tabs. className = "fcpython-quiz-steps" ;
tabs. setAttribute ("role" , "tablist" );
tabs. setAttribute ("aria-label" , "Quiz questions" );
progress. appendChild (tabs);
container. appendChild (progress);
const questions = document . createElement ("div" );
questions. className = "fcpython-quiz-questions" ;
const feedbackNodes = [];
const questionPanels = [];
const stepButtons = [];
let currentQuestion = 0 ;
function setStepStatus (questionIndex, status) {
const step = stepButtons[questionIndex];
const question = quiz. questions [questionIndex];
const statuses = {
answered : "answered" ,
correct : "correct" ,
incorrect : "incorrect" ,
unanswered : "not answered" ,
};
step. classList . remove (
"is-answered" ,
"is-correct" ,
"is-incorrect" ,
"is-unanswered"
);
if (status) {
step. classList . add (`is- ${ status} ` );
}
const statusLabel = status ? `, ${ statuses[status]} ` : "" ;
step. setAttribute (
"aria-label" ,
`Question ${ questionIndex + 1 } : ${ question. prompt }${ statusLabel} `
);
}
function showQuestion (questionIndex, focusPanel = false ) {
currentQuestion = Math . max (
0 ,
Math . min (questionIndex, quiz. questions . length - 1 )
);
questionPanels. forEach ((panel, index) => {
panel. hidden = index !== currentQuestion;
});
stepButtons. forEach ((step, index) => {
const isCurrent = index === currentQuestion;
step. classList . toggle ("is-current" , isCurrent);
step. setAttribute ("aria-selected" , String (isCurrent));
step. tabIndex = isCurrent ? 0 : - 1 ;
});
counter. textContent = `Question ${ currentQuestion + 1 } of ${ quiz. questions . length } ` ;
const selected = questionPanels[currentQuestion]. querySelector (
"input[type='radio']:checked"
);
const isLastQuestion = currentQuestion === quiz. questions . length - 1 ;
previous. hidden = currentQuestion === 0 ;
next. hidden = isLastQuestion || ! selected;
check. hidden = ! isLastQuestion;
if (focusPanel) {
questionPanels[currentQuestion]. focus ();
}
}
quiz. questions . forEach ((question, questionIndex) => {
const tabId = ` ${ quiz. id } -question-tab- ${ questionIndex + 1 } ` ;
const panelId = ` ${ quiz. id } -question-panel- ${ questionIndex + 1 } ` ;
const step = document . createElement ("button" );
step. type = "button" ;
step. className = "fcpython-quiz-step" ;
step. id = tabId;
step. textContent = String (questionIndex + 1 );
step. setAttribute ("role" , "tab" );
step. setAttribute ("aria-controls" , panelId);
step. setAttribute ("aria-selected" , "false" );
step. tabIndex = - 1 ;
step. addEventListener ("click" , () => showQuestion (questionIndex));
step. addEventListener ("keydown" , (event ) => {
let destination = null ;
if (event . key === "ArrowRight" ) {
destination = (questionIndex + 1 ) % quiz. questions . length ;
} else if (event . key === "ArrowLeft" ) {
destination =
(questionIndex - 1 + quiz. questions . length ) % quiz. questions . length ;
} else if (event . key === "Home" ) {
destination = 0 ;
} else if (event . key === "End" ) {
destination = quiz. questions . length - 1 ;
}
if (destination !== null ) {
event . preventDefault ();
showQuestion (destination);
stepButtons[destination]. focus ();
}
});
stepButtons. push (step);
tabs. appendChild (step);
setStepStatus (questionIndex, "" );
const panel = document . createElement ("div" );
panel. className = "fcpython-quiz-panel" ;
panel. id = panelId;
panel. setAttribute ("role" , "tabpanel" );
panel. setAttribute ("aria-labelledby" , tabId);
panel. tabIndex = - 1 ;
const fieldset = document . createElement ("fieldset" );
fieldset. className = "fcpython-quiz-question" ;
const legend = document . createElement ("legend" );
legend. textContent = question. prompt ;
fieldset. appendChild (legend);
question. options . forEach ((option, optionIndex) => {
const label = document . createElement ("label" );
label. className = "fcpython-quiz-option" ;
const input = document . createElement ("input" );
input. type = "radio" ;
input. name = ` ${ quiz. id } - ${ question. id } ` ;
input. value = String (optionIndex);
input. addEventListener ("change" , () => {
setStepStatus (questionIndex, "answered" );
feedbackNodes[questionIndex]. textContent = "" ;
feedbackNodes[questionIndex]. className = "fcpython-quiz-feedback" ;
score. textContent = "" ;
showQuestion (questionIndex);
});
const text = document . createElement ("span" );
text. textContent = option;
label. appendChild (input);
label. appendChild (text);
fieldset. appendChild (label);
});
const feedback = document . createElement ("p" );
feedback. className = "fcpython-quiz-feedback" ;
feedback. setAttribute ("aria-live" , "polite" );
feedbackNodes. push (feedback);
fieldset. appendChild (feedback);
panel. appendChild (fieldset);
questionPanels. push (panel);
questions. appendChild (panel);
});
container. appendChild (questions);
const actions = document . createElement ("div" );
actions. className = "fcpython-quiz-actions" ;
const previous = document . createElement ("button" );
previous. type = "button" ;
previous. className = "fcpython-quiz-secondary" ;
previous. textContent = "Previous" ;
previous. addEventListener ("click" , () => {
showQuestion (currentQuestion - 1 , true );
});
const next = document . createElement ("button" );
next. type = "button" ;
next. textContent = "Next question" ;
next. addEventListener ("click" , () => {
showQuestion (currentQuestion + 1 , true );
});
const check = document . createElement ("button" );
check. type = "button" ;
check. textContent = "Check answers" ;
const reset = document . createElement ("button" );
reset. type = "button" ;
reset. className = "fcpython-quiz-secondary fcpython-quiz-reset" ;
reset. textContent = "Reset" ;
const score = document . createElement ("p" );
score. className = "fcpython-quiz-score" ;
score. setAttribute ("aria-live" , "polite" );
check. addEventListener ("click" , () => {
let correctCount = 0 ;
let firstQuestionToReview = null ;
quiz. questions . forEach ((question, questionIndex) => {
const selected = container. querySelector (
`input[name=" ${ quiz. id } - ${ question. id } "]:checked`
);
const feedback = feedbackNodes[questionIndex];
if (! selected) {
feedback. textContent = "Choose an answer before checking." ;
feedback. className = "fcpython-quiz-feedback" ;
setStepStatus (questionIndex, "unanswered" );
if (firstQuestionToReview === null ) {
firstQuestionToReview = questionIndex;
}
return ;
}
const selectedIndex = Number (selected. value );
if (selectedIndex === question. answer_index ) {
correctCount += 1 ;
feedback. textContent = `✅ Correct. ${ question. explanation } ` ;
feedback. className = "fcpython-quiz-feedback is-correct" ;
setStepStatus (questionIndex, "correct" );
} else {
const answer = question. options [question. answer_index ];
feedback. textContent = `❌ Not yet. Correct answer: ${ answer} . ${ question. explanation } ` ;
feedback. className = "fcpython-quiz-feedback is-incorrect" ;
setStepStatus (questionIndex, "incorrect" );
if (firstQuestionToReview === null ) {
firstQuestionToReview = questionIndex;
}
}
});
score. textContent = `Score: ${ correctCount} / ${ quiz. questions . length } ` ;
if (firstQuestionToReview !== null ) {
showQuestion (firstQuestionToReview, true );
}
});
reset. addEventListener ("click" , () => {
container. querySelectorAll ("input[type='radio']" ). forEach ((input) => {
input. checked = false ;
});
feedbackNodes. forEach ((feedback) => {
feedback. textContent = "" ;
feedback. className = "fcpython-quiz-feedback" ;
});
stepButtons. forEach ((_, questionIndex) => {
setStepStatus (questionIndex, "" );
});
score. textContent = "" ;
showQuestion (0 , true );
});
actions. appendChild (previous);
actions. appendChild (next);
actions. appendChild (check);
actions. appendChild (reset);
container. appendChild (actions);
container. appendChild (score);
showQuestion (0 );
return container;
}
11. Build the game-world archive
Preserve the supplied source tuple. Build ordered display, direct lookup, unique hazard, and climate-group views without mutating any record.
source_world = (
{
"id" : "garden" ,
"coordinate" : (4 , 7 ),
"climate" : "cool" ,
"hazards" : {"fog" },
"guide" : "Nova" ,
},
{
"id" : "vault" ,
"coordinate" : (9 , 2 ),
"climate" : "warm" ,
"hazards" : {"lock" , "darkness" },
"guide" : None ,
},
{
"id" : "tower" ,
"coordinate" : (2 , 8 ),
"climate" : "cool" ,
"hazards" : set (),
},
)
region_ids = []
regions_by_id = {}
all_hazards = set ()
ids_by_climate = {}
guide_states = {}
vault_row = None
vault_column = None
Requirements:
preserve source order in region_ids;
map each stable ID to its complete record;
combine every unique hazard;
group IDs by climate while preserving source order within each group;
classify each guide state as "named", "none", or "absent" using the supplied scaffold below; and
unpack the vault coordinate into named row and column values.
The classification uses simple conditional syntax supplied from the next unit:
for region in source_world:
region_id = region["id" ]
if "guide" not in region:
guide_states[region_id] = "absent"
elif region["guide" ] is None :
guide_states[region_id] = "none"
else :
guide_states[region_id] = "named"
Complete the other views around that scaffold, then run:
assert region_ids == ["garden" , "vault" , "tower" ]
assert list (regions_by_id) == ["garden" , "vault" , "tower" ]
assert regions_by_id["garden" ]["coordinate" ] == (4 , 7 )
assert all_hazards == {"fog" , "lock" , "darkness" }
assert ids_by_climate == {
"cool" : ["garden" , "tower" ],
"warm" : ["vault" ],
}
assert guide_states == {
"garden" : "named" ,
"vault" : "none" ,
"tower" : "absent" ,
}
assert (vault_row, vault_column) == (9 , 2 )
assert source_world == (
{
"id" : "garden" ,
"coordinate" : (4 , 7 ),
"climate" : "cool" ,
"hazards" : {"fog" },
"guide" : "Nova" ,
},
{
"id" : "vault" ,
"coordinate" : (9 , 2 ),
"climate" : "warm" ,
"hazards" : {"lock" , "darkness" },
"guide" : None ,
},
{
"id" : "tower" ,
"coordinate" : (2 , 8 ),
"climate" : "cool" ,
"hazards" : set (),
},
)
Boundary variation: add a fourth well-shaped cool region with one repeated and one new hazard. Predict which output collections preserve the duplicate, discard it, or add another ordered ID before rerunning.
Hint: let every derived collection answer one named question
In one traversal, append the ID, assign the complete record by ID, update the hazard set from the record’s set, and create/append the climate group list. Use the provided guide-state scaffold in that same traversal or a second one. Retrieve the vault record from the finished lookup and unpack its coordinate.
Show one complete solution after attempting the archive
region_ids = []
regions_by_id = {}
all_hazards = set ()
ids_by_climate = {}
guide_states = {}
for region in source_world:
region_id = region["id" ]
climate = region["climate" ]
region_ids.append(region_id)
regions_by_id[region_id] = region
all_hazards.update(region["hazards" ])
if climate not in ids_by_climate:
ids_by_climate[climate] = []
ids_by_climate[climate].append(region_id)
if "guide" not in region:
guide_states[region_id] = "absent"
elif region["guide" ] is None :
guide_states[region_id] = "none"
else :
guide_states[region_id] = "named"
vault_row, vault_column = regions_by_id["vault" ]["coordinate" ]
Every derived collection has a distinct purpose. The lookup intentionally shares the immutable-by-convention source records, and the code never mutates them. The hazard view stores only unique strings, while the ordered ID and grouped-ID lists preserve the order relevant to their displays.
12. Explain the data shape
How do you determine whether the next brackets need an index or a key?
What different broken promises do TypeError, KeyError, and IndexError reveal along a nested path?
Why are absent, stored None, and empty collection values not interchangeable?
How can a shallow outer copy still expose source data to nested mutation?
When is a secondary dictionary lookup worth keeping beside an ordered list?
Key points
Describe the outer type and every nested level before writing a long access path.
Apply each index or key to the value immediately on its left; store intermediate values when the current type is unclear.
A list of records, keyed record dictionary, grouped dictionary of lists, and position-based grid answer different access questions.
Preserve absent, stored None, and empty values when they express different states.
Consistent record fields and tuple lengths make traversal predictable; failures provide evidence about the malformed level.
Parallel lists can drift; complete records preserve field relationships.
A shallow copy separates an outer container but can retain nested aliases.
Ordered source and direct-lookup views can coexist when ownership is explicit.
Back to top