1. Find a creature by ID rather than by position
A field guide should answer “What do we know about creature mossling?” A list could store creature records, but repeated lookup would require finding the right position. A dictionary connects a stable key directly to a value :
creature_energy = {
"mossling" : 30 ,
"skyfin" : 55 ,
"emberfox" : 42 ,
}
print (creature_energy["skyfin" ])
print (len (creature_energy))
print (type (creature_energy).__name__ )
The key "skyfin" maps to integer value 55. Keys are unique within one dictionary. Values may repeat.
Python dictionaries preserve insertion order , but their central purpose is lookup by key, not access by numeric position. If visible order is the only requirement, a list may communicate that job more directly.
An empty pair of braces is a dictionary, not a set:
empty_catalog = {}
also_empty = dict ()
assert type (empty_catalog) is dict
assert empty_catalog == also_empty
This lesson will help you answer:
Is a key absent, or is its stored value deliberately None?
Does an operation mutate the dictionary or create a new mapping?
What do keys, values, and item views supply during traversal?
When does a dictionary represent one record, a lookup table, a counter, or a grouping?
2. Direct lookup makes missing keys visible
Square brackets state that the key must exist:
creature_energy = {"mossling" : 30 , "skyfin" : 55 }
print (creature_energy["mossling" ])
print ("skyfin" in creature_energy)
print ("dragon" not in creature_energy)
Dictionary membership checks keys , not values:
print ("mossling" in creature_energy)
print (30 in creature_energy)
print (30 in creature_energy.values())
The results are True, False, and True. State the side of the mapping you intend to search.
An absent direct key raises KeyError:
print (creature_energy["dragon" ])
The traceback includes the requested key. That failure is useful when absence means the program’s data contract is broken.
.get() supports expected absence
creature_energy = {"mossling" : 30 , "skyfin" : 55 }
print (creature_energy.get("skyfin" ))
print (creature_energy.get("dragon" ))
print (creature_energy.get("dragon" , 0 ))
.get(key) returns None for an absent key; .get(key, default) returns the provided default. It does not add that key to the dictionary.
Missing and stored-as-None are different states
observations = {
"mossling" : "near the river" ,
"skyfin" : None ,
}
print (observations.get("skyfin" ))
print (observations.get("dragon" ))
print ("skyfin" in observations)
print ("dragon" in observations)
Both .get() calls print None, but membership distinguishes the states. The skyfin key exists and deliberately stores a missing observation; dragon has no entry. Use key in mapping when that distinction changes the program’s meaning.
A dictionary connects unique lookup keys to values; the name references the complete mapping rather than one pair.
flowchart LR
A["creature_energy"] --> B["dictionary"]
B --> C["mossling key"]
C --> D["30 value"]
B --> E["skyfin key"]
E --> F["55 value"]
Checkpoint: lookup and missing states
{
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;
}
3. Add and replace pairs with assignment
Assignment through a key adds an absent pair or replaces an existing value:
creature_energy = {"mossling" : 30 , "skyfin" : 55 }
creature_energy["emberfox" ] = 42
creature_energy["mossling" ] = 35
print (creature_energy)
The dictionary now has three keys. Replacing mossling does not move it to the end; its original insertion position remains.
.update() applies several pairs in place:
catalog = {"mossling" : 30 , "skyfin" : 55 }
changes = {"skyfin" : 60 , "emberfox" : 42 }
result = catalog.update(changes)
print (catalog)
print (result)
The existing skyfin value is replaced, emberfox is added, and the method returns None.
The merge operator creates a new dictionary:
original = {"mossling" : 30 , "skyfin" : 55 }
changes = {"skyfin" : 60 , "emberfox" : 42 }
merged = original | changes
print (original)
print (merged)
When both operands contain the same key, the right operand’s value wins. The original mapping remains unchanged.
The in-place merge operator changes its left operand:
working_catalog = {"mossling" : 30 }
working_catalog |= {"skyfin" : 55 }
print (working_catalog)
Choose from the state requirement: use | when both inputs must remain available; use update/assignment when the existing working mapping is meant to change.
4. Remove a pair and know what is returned
pop(key) removes a requested key and returns its value:
catalog = {"mossling" : 30 , "skyfin" : 55 , "emberfox" : 42 }
retired_energy = catalog.pop("skyfin" )
print (retired_energy)
print (catalog)
An absent key raises KeyError, unless you supply a default:
missing_energy = catalog.pop("dragon" , None )
print (missing_energy)
No pair is added. Use a default only when absence is an expected state; it can otherwise hide a misspelled key.
del removes without returning the value:
catalog = {"mossling" : 30 , "skyfin" : 55 }
del catalog["mossling" ]
print (catalog)
popitem() removes and returns the most recently inserted remaining pair:
catalog = {"mossling" : 30 , "skyfin" : 55 , "emberfox" : 42 }
last_pair = catalog.popitem()
print (last_pair)
print (type (last_pair).__name__ )
The result is the tuple ('emberfox', 42). This is useful for last-in processing, not for choosing an arbitrary key. Calling it on an empty dictionary raises KeyError.
clear() removes all pairs and returns None.
5. Views stay connected to the dictionary
.keys(), .values(), and .items() expose different sides of a mapping:
catalog = {"mossling" : 30 , "skyfin" : 55 }
keys = catalog.keys()
values = catalog.values()
pairs = catalog.items()
print (keys)
print (values)
print (pairs)
These are view objects , not frozen list snapshots. If the mapping changes, the views reflect its current state:
keys = catalog.keys()
catalog["emberfox" ] = 42
print (keys)
print ("emberfox" in keys)
Create a list explicitly when you need a separate positional snapshot:
key_snapshot = list (catalog.keys())
catalog["cloudwhale" ] = 90
print (key_snapshot)
print (list (catalog.keys()))
Direct traversal supplies keys
catalog = {"mossling" : 30 , "skyfin" : 55 }
for creature_id in catalog:
print (creature_id)
Use .values() when only values matter, or .items() to unpack key-value tuples:
for creature_id, energy in catalog.items():
print (f" { creature_id} : { energy} " )
Do not write for key, value in catalog: and expect pairs. Direct traversal receives each key; a string key might then be incorrectly unpacked character by character or fail from the wrong length.
Checkpoint: updates, removals, and views
{
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;
}
6. Keys must be unique and hashable
Writing a duplicate key in one literal retains the last value:
levels = {"mossling" : 1 , "mossling" : 2 }
print (levels)
The dictionary contains one pair: "mossling": 2. Duplicate key syntax is not a way to store several values; use a list as the one value when grouping is the job.
Common immutable scalar values—strings, integers, and suitable tuples—can be keys:
terrain = {
(12 , 7 ): "vault" ,
(5 , 3 ): "garden" ,
}
print (terrain[(12 , 7 )])
A list cannot be a key:
invalid_catalog = {[12 , 7 ]: "vault" }
Python raises TypeError: unhashable type: 'list'. A key must have a stable hash and equality behavior while it participates in the dictionary. Unit 6 develops the full rule; for now, stable IDs and immutable coordinate tuples are safe, readable choices.
A dictionary is designed to find a value from its key without scanning every pair in the ordinary case. Searching for an arbitrary value still requires inspecting values unless the program maintains a second reverse lookup. Use this cost intuition to choose a shape; Unit 7 treats algorithmic efficiency more formally.
7. One dictionary can represent one named record
Keys make heterogeneous fields readable:
creature = {
"id" : "mossling" ,
"energy" : 30 ,
"habitat" : "riverbank" ,
"discovered" : True ,
"nickname" : None ,
}
print (creature["habitat" ])
creature["energy" ] = 35
creature["energy"] explains its meaning more clearly than creature[1] when the record has several fields. Keep field names consistent across similar records; Lesson 7 shows how malformed nested shapes fail.
An optional field may be absent, explicitly None, or an empty collection. Those states should be chosen, not accidentally collapsed:
record = {"id" : "skyfin" , "nickname" : None , "sightings" : []}
assert "notes" not in record
assert "nickname" in record and record["nickname" ] is None
assert "sightings" in record and record["sightings" ] == []
8. Dictionaries also count and group
For a counter, each distinct item becomes a key and its occurrence count becomes the value:
regions = ["north" , "east" , "north" , "west" , "north" ]
counts = {}
for region in regions:
counts[region] = counts.get(region, 0 ) + 1
print (counts)
Trace the first "north": it is absent, so .get(..., 0) supplies zero; adding one stores 1. The later occurrences retrieve and increment the existing value. The loop pattern itself is developed further in Unit 4; here the important model is {value_being_counted: count}.
A grouping dictionary maps each category to a list of complete items:
observations = (
("north" , "mossling" ),
("east" , "skyfin" ),
("north" , "emberfox" ),
)
groups = {}
for region, creature_id in observations:
if region not in groups:
groups[region] = []
groups[region].append(creature_id)
print (groups)
After the explicit steps make sense, .setdefault() can express “retrieve the list, creating it if absent”:
groups = {}
for region, creature_id in observations:
groups.setdefault(region, []).append(creature_id)
.setdefault(key, default) inserts the default only when the key is absent and then returns the stored value. It is concise, but do not use it before you can explain which list receives .append().
9. A shallow copy separates only the outer mapping
source = {
"name" : "mossling" ,
"skills" : ["hide" , "heal" ],
}
snapshot = source.copy()
snapshot["name" ] = "elder mossling"
snapshot["skills" ].append("glow" )
print (source)
print (snapshot)
The top-level "name" fields differ because the outer dictionaries are separate. Both "skills" keys still refer to the same nested list, so both displays include "glow". dict(source) and {**source} also make shallow outer copies. Unit 6 owns the choice between shared nested state, deliberate reconstruction, and deep copying.
Checkpoint: records, counters, and grouping
{
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;
}
10. Build a creature codex
Preserve the source tuple of records while producing a direct lookup, a working update, habitat counts, and grouped creature IDs.
source_creatures = (
{"id" : "mossling" , "energy" : 30 , "habitat" : "river" },
{"id" : "skyfin" , "energy" : 55 , "habitat" : "cliffs" },
{"id" : "emberfox" , "energy" : 42 , "habitat" : "river" },
)
creatures_by_id = {}
working_mossling = None
habitat_counts = {}
creatures_by_habitat = {}
removed_skyfin = None
Complete these stages:
traverse the source and map each creature ID to its complete record;
create a separate outer dictionary for the mossling record and increase only that working energy to 35;
count records by habitat;
group creature IDs into lists by habitat;
create a separate working copy of the lookup and pop() the skyfin record; store the returned record in removed_skyfin; and
prove that the source tuple and original lookup still contain their original data.
assert list (creatures_by_id) == ["mossling" , "skyfin" , "emberfox" ]
assert creatures_by_id["skyfin" ]["energy" ] == 55
assert working_mossling == {
"id" : "mossling" ,
"energy" : 35 ,
"habitat" : "river" ,
}
assert source_creatures[0 ]["energy" ] == 30
assert habitat_counts == {"river" : 2 , "cliffs" : 1 }
assert creatures_by_habitat == {
"river" : ["mossling" , "emberfox" ],
"cliffs" : ["skyfin" ],
}
assert removed_skyfin["id" ] == "skyfin"
assert "skyfin" in creatures_by_id
assert source_creatures == (
{"id" : "mossling" , "energy" : 30 , "habitat" : "river" },
{"id" : "skyfin" , "energy" : 55 , "habitat" : "cliffs" },
{"id" : "emberfox" , "energy" : 42 , "habitat" : "river" },
)
Then add a fourth source record in habitat "cliffs". Rerun cleanly and update the expected order, count, and group without hard-coding a special case for that creature.
Hint: build each dictionary around one lookup question
For the direct lookup, assign record["id"] as a key and the complete record as its value. Copy the mossling record before replacing its energy. For each habitat, use .get(habitat, 0) + 1 in the counter and create a list before appending in the grouping dictionary. Pop from a copy of the complete lookup.
Show one complete solution after attempting the lab
source_creatures = (
{"id" : "mossling" , "energy" : 30 , "habitat" : "river" },
{"id" : "skyfin" , "energy" : 55 , "habitat" : "cliffs" },
{"id" : "emberfox" , "energy" : 42 , "habitat" : "river" },
)
creatures_by_id = {}
habitat_counts = {}
creatures_by_habitat = {}
for record in source_creatures:
creature_id = record["id" ]
habitat = record["habitat" ]
creatures_by_id[creature_id] = record
habitat_counts[habitat] = habitat_counts.get(habitat, 0 ) + 1
if habitat not in creatures_by_habitat:
creatures_by_habitat[habitat] = []
creatures_by_habitat[habitat].append(creature_id)
working_mossling = creatures_by_id["mossling" ].copy()
working_mossling["energy" ] = 35
working_lookup = creatures_by_id.copy()
removed_skyfin = working_lookup.pop("skyfin" )
The lookup values deliberately share the untouched source records. The lab never mutates those nested records. The one edited record is copied first, and the one removed pair is popped from a separate outer lookup.
11. Explain the mapping decisions
When is square-bracket lookup better evidence than .get()?
Why does dictionary membership not answer whether a value occurs?
What do update() and | do differently to their input mappings?
Why is a dictionary view different from list(dictionary)?
Which question is answered by a record, a counter, a grouping, and a direct lookup?
Key points
Dictionaries map unique hashable keys to values and preserve insertion order.
Square brackets require a key; .get() supports expected absence; membership distinguishes an absent key from a stored None.
Assignment and update() mutate; | creates a new merged dictionary; the right-side value wins for duplicate keys.
pop() returns a removed value, popitem() returns the latest pair, and del removes without returning the value.
Keys, values, and item views remain connected to the mapping; direct traversal supplies keys.
Dictionaries can model named records, keyed lookups, counters, and groups.
.copy() creates a separate outer mapping but retains nested value references.
Back to top