1. A loop needs a product, not only repeated output
Unit 3 showed how a for loop receives collection values. Most useful loops do more than print them: they build a result that remains after traversal.
distances = [4 , 7 , 3 ]
total_distance = 0
for distance in distances:
total_distance += distance
print (total_distance)
assert total_distance == 14
total_distance is an accumulator : state initialized before the loop and updated once for each relevant item. Its job can be stated precisely:
After each iteration, total_distance equals the sum of distances already visited.
That statement is a practical loop invariant . It helps you decide where to initialize the name, what each iteration must change, and what the final value means.
Questions this lesson will answer
Which starting value fits a total, count, list, dictionary, or set result?
How do transform and filter patterns differ?
How can one traversal preserve both accepted and rejected evidence?
How do counters and grouped records use dictionaries?
When is a built-in clearer and safer than a manual loop?
A result-building loop starts with an empty or neutral result, receives one item, and updates the result before moving to the next item.
flowchart LR
A[Initialize result once] --> B[Receive next item]
B --> C{Use this item?}
C -- Yes --> D[Update result]
C -- No --> E[Preserve or skip evidence]
D --> F{Items remain?}
E --> F
F -- Yes --> B
F -- No --> G[Use final result]
2. Initialize once, before traversal
Moving initialization inside the loop erases earlier work:
distances = [4 , 7 , 3 ]
for distance in distances:
total_distance = 0
total_distance += distance
print (total_distance)
The displayed value is 3, the final item, because each iteration resets the total. On an empty list, the body never runs and total_distance would not exist at all.
The correct pattern gives the result a meaningful empty-input value:
total_distance = 0
for distance in []:
total_distance += distance
assert total_distance == 0
Common initial values follow the promised result:
numeric total
0
total += value
number of matches
0
count += 1
ordered transformed or filtered values
[]
result.append(value)
values grouped or counted by key
{}
update one key
unique observed values
set()
seen.add(value)
every item is valid
True
set false when one fails
at least one item matches
False
set true when one matches
An initial value is not boilerplate. It defines what the result means before any input arrives.
3. Total and count answer different questions
A total adds item values. A count adds one for each qualifying item:
readings = [8 , - 2 , 0 , 5 , - 1 ]
usable_total = 0
usable_count = 0
for reading in readings:
if reading >= 0 :
usable_total += reading
usable_count += 1
print (usable_total, usable_count)
assert usable_total == 13
assert usable_count == 3
The zero reading contributes nothing to the total but still contributes one to the count. Using if reading: would incorrectly exclude it.
The average requires both accumulators and an empty-case policy:
if usable_count > 0 :
usable_average = usable_total / usable_count
else :
usable_average = None
None says no average exists. Returning zero would confuse “no readings” with “readings whose average is zero.”
Checkpoint: accumulator foundations
{
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;
}
5. Filter by appending only accepted items
A filter can produce zero or one output per input:
readings = [8 , - 2 , 0 , 5 , - 1 ]
usable = []
for reading in readings:
if reading >= 0 :
usable.append(reading)
assert usable == [8 , 0 , 5 ]
The append belongs inside the conditional because acceptance decides whether the item enters the result. If it were aligned with if, every item would be added.
Silently dropping rejected data may make debugging harder. Preserve it when the program needs an audit trail:
usable = []
rejected = []
for reading in readings:
if reading >= 0 :
usable.append(reading)
else :
rejected.append(reading)
assert usable == [8 , 0 , 5 ]
assert rejected == [- 2 , - 1 ]
Now every source item belongs to exactly one result. A useful invariant is:
After each iteration, len(usable) + len(rejected) equals the number of items visited so far.
7. Count occurrences with a dictionary
A counter connects each observed value with how many times it appeared:
signals = ["blue" , "red" , "blue" , "gold" , "red" , "blue" ]
counts = {}
for signal in signals:
if signal in counts:
counts[signal] += 1
else :
counts[signal] = 1
assert counts == {"blue" : 3 , "red" : 2 , "gold" : 1 }
On the first occurrence, create the key with count 1. Later occurrences update the existing value. Dictionary .get() can express the same default:
counts = {}
for signal in signals:
counts[signal] = counts.get(signal, 0 ) + 1
Read the right side first: obtain the existing count or zero, add one, then store the new count at the same key.
8. Group complete records by a key
Grouping maps one key to several original records:
observations = [
{"zone" : "north" , "species" : "owl" },
{"zone" : "south" , "species" : "fox" },
{"zone" : "north" , "species" : "moth" },
]
by_zone = {}
for observation in observations:
zone = observation["zone" ]
if zone not in by_zone:
by_zone[zone] = []
by_zone[zone].append(observation)
assert [item["species" ] for item in by_zone["north" ]] == ["owl" , "moth" ]
The empty list must be created separately for each new key. Assigning one shared list to many keys would mix groups. Unit 6 develops aliasing in depth; for now, create the group exactly when its key first appears.
9. A set accumulator keeps unique observations
species = ["owl" , "fox" , "owl" , "moth" ]
unique_species = set ()
for name in species:
unique_species.add(name)
assert unique_species == {"owl" , "fox" , "moth" }
The set answers uniqueness and membership questions. It does not preserve a numeric position. If you need unique values in first-seen order, combine a set for fast “seen?” checks with a list for ordered output:
seen = set ()
first_seen = []
for name in species:
if name not in seen:
seen.add(name)
first_seen.append(name)
assert first_seen == ["owl" , "fox" , "moth" ]
The two accumulators have different jobs and stay synchronized inside the same branch.
10. Validation accumulates a Boolean claim
To report whether every value is in range:
levels = [4 , 8 , 11 ]
all_valid = True
for level in levels:
if not 0 <= level <= 10 :
all_valid = False
assert all_valid is False
all_valid starts true because no visited item has disproved the claim. Once false, it stays false. If you also need invalid values, collect them instead and derive all_valid = not invalid_levels afterward.
A separate “at least one” claim starts false and becomes true when a match is seen. Lesson 6 shows how a search can stop early when no additional evidence is needed.
11. Use positions and pairs only when the result needs them
enumerate() supplies a position with each value:
stops = ["dock" , "ridge" , "clinic" ]
numbered = []
for number, stop in enumerate (stops, start= 1 ):
numbered.append(f" { number} . { stop} " )
assert numbered == ["1. dock" , "2. ridge" , "3. clinic" ]
zip() supplies aligned values from several collections and stops at the shortest:
names = ["north" , "east" , "south" ]
readings = [4 , 7 , 2 ]
station_readings = {}
for name, reading in zip (names, readings, strict= True ):
station_readings[name] = reading
assert station_readings == {"north" : 4 , "east" : 7 , "south" : 2 }
Unit 3 taught those supply mechanics. Here they serve a result-building job. strict=True raises ValueError when one input ends early, preventing silent loss when aligned records are required.
12. Prefer a built-in when it says the complete job
Python already names common reductions:
values = [4 , 7 , 3 ]
assert sum (values) == 14
assert len (values) == 3
assert min (values) == 3
assert max (values) == 7
sum(values) is clearer than a manual total when no special filtering or trace is needed. min([]) and max([]) raise ValueError because an empty collection has no smallest or largest item. Supply a policy when empty input is expected:
values = []
smallest = min (values) if values else None
assert smallest is None
Do not replace a rich loop with a pile of built-ins if that hides the rule. Use the simplest operation that expresses the entire required artifact.
Checkpoint: structured results
{
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;
}
13. Do not change the list you are currently traversing
Removing items from a list while a for loop visits it can skip values because positions shift:
numbers = [1 , 2 , 2 , 3 ]
for number in numbers:
if number == 2 :
numbers.remove(number)
print (numbers)
The result still contains a 2. After the first removal, the second 2 shifts into a position the loop has already advanced past.
Build a new result instead:
numbers = [1 , 2 , 2 , 3 ]
without_twos = []
for number in numbers:
if number != 2 :
without_twos.append(number)
assert numbers == [1 , 2 , 2 , 3 ]
assert without_twos == [1 , 3 ]
Preserving the source also makes before-and-after assertions possible.
14. Build an expedition report
Start with this source evidence:
observations = [
{"station" : "north" , "species" : "owl" , "count" : 2 , "valid" : True },
{"station" : "east" , "species" : "fox" , "count" : 1 , "valid" : True },
{"station" : "north" , "species" : "owl" , "count" : - 1 , "valid" : False },
{"station" : "south" , "species" : "moth" , "count" : 4 , "valid" : True },
{"station" : "east" , "species" : "owl" , "count" : 3 , "valid" : True },
]
accepted = []
rejected = []
total_animals = 0
species_counts = {}
by_station = {}
unique_species = set ()
In one for loop:
place valid non-negative records in accepted and all others in rejected;
update the total only for accepted records;
add each accepted record’s count to species_counts;
group accepted species names by station, preserving observation order; and
collect unique accepted species.
Then derive station_lines with enumerate(..., start=1) over the station keys in their first-seen order. Run:
assert observations[2 ]["count" ] == - 1
assert len (accepted) == 4
assert rejected == [observations[2 ]]
assert total_animals == 10
assert species_counts == {"owl" : 5 , "fox" : 1 , "moth" : 4 }
assert by_station == {
"north" : ["owl" ],
"east" : ["fox" , "owl" ],
"south" : ["moth" ],
}
assert unique_species == {"owl" , "fox" , "moth" }
assert station_lines == ["1. north" , "2. east" , "3. south" ]
Hint: let the validity branch own every update
Append rejected records in the first branch and accepted records in the other. Only the accepted branch should update totals, counters, groups, and the set. Use .get(species, 0) for totals by species, and create a station list only when its key is new.
Show one complete solution after attempting the lab
observations = [
{"station" : "north" , "species" : "owl" , "count" : 2 , "valid" : True },
{"station" : "east" , "species" : "fox" , "count" : 1 , "valid" : True },
{"station" : "north" , "species" : "owl" , "count" : - 1 , "valid" : False },
{"station" : "south" , "species" : "moth" , "count" : 4 , "valid" : True },
{"station" : "east" , "species" : "owl" , "count" : 3 , "valid" : True },
]
accepted = []
rejected = []
total_animals = 0
species_counts = {}
by_station = {}
unique_species = set ()
for observation in observations:
if not observation["valid" ] or observation["count" ] < 0 :
rejected.append(observation)
else :
accepted.append(observation)
total_animals += observation["count" ]
species = observation["species" ]
species_counts[species] = (
species_counts.get(species, 0 ) + observation["count" ]
)
unique_species.add(species)
station = observation["station" ]
if station not in by_station:
by_station[station] = []
by_station[station].append(species)
station_lines = []
for number, station in enumerate (by_station, start= 1 ):
station_lines.append(f" { number} . { station} " )
All result updates are controlled by the acceptance decision, and the original records remain available for comparison.
15. Explain the accumulator choices
What does each accumulator mean before the first item arrives?
Why does zero count as one accepted reading while adding zero to a total?
How does collecting rejected records improve the audit trail?
Why does each group key need its own list?
When is sum() clearer than a manual loop, and when does a loop reveal needed filtering or evidence?
Key points
Initialize a result once before traversal with a value that correctly describes empty input.
A total adds values; a count adds one per match; transformation and filtering produce different numbers of outputs per input.
Lists preserve result order, dictionaries count or group by key, and sets preserve uniqueness.
State what an accumulator means after each iteration; that invariant guides initialization and updates.
Preserve rejected evidence when loss matters, and avoid mutating the list being traversed.
Prefer a built-in when it clearly states the whole job and its empty behavior is acceptable.
Back to top