Move from a hand-solved case to traceable pseudocode, cohesive function contracts, and an incremental Python implementation with checks at every stage.
Lesson 1 ended with a contract for choosing a moon-base supply pod. A contract says what must be observable; it does not yet say how the program will reach the result. This lesson supplies that bridge.
Keep these questions near your code:
What did you do by hand to solve one small case?
Which value must survive while you inspect later records?
Where can the work be split into independently checkable responsibilities?
Does the pseudocode show every decision, update, and stopping point?
What is the smallest end-to-end behavior you can make pass now?
LUNA-4 is usable, so it becomes the current choice.
NOVA-2 is usable and ties on oxygen. Its lower mass replaces LUNA-4.
DUST-9 has more oxygen but is damaged, so it cannot replace the choice.
Return "NOVA-2".
The manual work reveals two responsibilities—eligibility and comparison—and one piece of persistent state: the current best usable pod.
2. Record what changes after every pod
A trace table makes the state visible:
Current pod
Usable?
Best before
Replace?
Best after
LUNA-4
yes
None
yes
LUNA-4
NOVA-2
yes
LUNA-4
yes: equal oxygen, lower mass
NOVA-2
DUST-9
no
NOVA-2
no
NOVA-2
Notice what the table does not require. It does not need a second list of usable pods, and it does not need to sort all records. A running choice is enough for this contract.
Try the exact-tie rule by changing the second record:
The second pod is not better, so the state should remain unchanged. This detail will determine whether the comparison uses < or <= for mass.
3. Name the shape after every stage
A pipeline is easier to reason about when each arrow has a known data shape.
The supply selector reads pod records, makes a choice, and formats a message without mixing those responsibilities.
flowchart LR
A[List of pod records] --> B[Eligibility decisions]
B --> C[Selected pod or no result]
C --> D[Pod ID or None]
D --> E[Display message]
You could build a full list of eligible records, but it is not required to find one winner. The conceptual stages still matter even when one loop combines the first two:
records -> eligibility decision -> running best -> returned ID -> message
Data-shape notes catch mismatches early. Returning an entire dictionary when the contract promises a string is wrong even if the selected record is correct.
4. Write pseudocode around decisions and state
Pseudocode describes the algorithm without demanding exact Python punctuation:
SET best pod to no resultFOR EACH pod in the input IF the pod is damaged SKIP it IF there is no best pod yet SET best pod to this pod OTHERWISE IF this pod has more oxygen SET best pod to this pod OTHERWISE IF oxygen ties and this pod has lower mass SET best pod to this podIF there is no best pod RETURN no resultRETURN the best pod's ID
Useful pseudocode contains:
the order in which items are visited;
every decision that can change behavior;
the state that survives between items;
the update rule;
the no-result behavior; and
the returned result.
This pseudocode is incomplete if it says only “find the best pod.” That phrase renames the goal without explaining how progress occurs.
Trace pseudocode before translating it
Use the table from Section 2. At each row, point to the pseudocode condition that explains Best after. If no condition explains a row, the plan is incomplete.
A damaged opening pod exposes another useful case:
The initial None must remain until an eligible record appears.
5. Give each responsibility a checkable interface
The pod program needs only a few functions:
def is_usable(pod):"""Return True when pod is not damaged."""returnnot pod["damaged"]def is_better(candidate, current):"""Return True when candidate should replace current."""if candidate["oxygen"] != current["oxygen"]:return candidate["oxygen"] > current["oxygen"]return candidate["mass"] < current["mass"]def choose_pod(pods):"""Return the best usable pod ID, or None."""passdef format_choice(pod_id):"""Return a message describing the selection result."""pass
Each docstring names observable behavior. is_better assumes both records are usable because choose_pod owns eligibility. An exact tie returns False because neither comparison uses equality as a reason to replace.
A helper is valuable when its name identifies a real decision and its result can be checked independently.
6. Avoid a giant function and meaningless fragments
This function mixes selection, formatting, and display:
def announce_pod(pods): best =Nonefor pod in pods:ifnot pod["damaged"]:if best isNoneor pod["oxygen"] > best["oxygen"]: best = podif best isNone:print("No pod available")else:print(f"Dispatch {best['id']}")
It also forgets the mass tie rule. A learner checking the printed message must determine whether the error came from eligibility, comparison, or formatting.
The opposite extreme is not better:
def pod_oxygen(pod):return pod["oxygen"]def greater_than(left, right):return left > rightdef pod_id(pod):return pod["id"]
These wrappers do not name domain decisions. They force a reader to jump among functions without making any responsibility clearer.
A useful split is neither “one function” nor “as many functions as possible.” Split where a name expresses a distinct rule, side effect, or data boundary.
The displayed output is useful to a person, while the returned string is useful to assertions, a notebook, a command-line interface, or another function.
Implement only the structure needed to select it and handle no result:
def choose_pod(pods):"""Return the best usable pod ID, or None.""" best =Nonefor pod in pods:ifnot is_usable(pod):continueif best isNone: best = podif best isNone:returnNonereturn best["id"]
This is a vertical slice: input enters the public function and the promised result comes out. The selector is not complete yet, but it has one complete, observable behavior.
9. Add one acceptance rule at a time
Extend the replacement decision without changing the public interface:
def choose_pod(pods):"""Return the best usable pod ID, or None.""" best =Nonefor pod in pods:ifnot is_usable(pod):continueif best isNoneor is_better(pod, best): best = podif best isNone:returnNonereturn best["id"]
If the fourth check fails but the first three pass, the likely problem is in comparison rather than empty handling or basic eligibility. Progressive checks turn a long task into a sequence of smaller questions.
10. Find the earliest stage with a wrong value
Suppose the full result is "LUNA-4" instead of "NOVA-2". Do not edit every function. Inspect in dependency order:
The earliest wrong observation points to the smallest responsibility worth investigating. Unit 8 will add traceback reading and debugger tools; here the important habit is to inspect the data where it first becomes incorrect.
WarningDo not change several stages at once
If you rewrite eligibility, comparison, selection, and formatting together, a passing result will not reveal which change repaired the behavior. Restore the last understood state and change one cause.
11. Protect the caller’s records
The running-best implementation only reads pods. Confirm that promise:
original_pods = [ {"id": "A", "oxygen": 4, "mass": 8, "damaged": False}, {"id": "B", "oxygen": 7, "mass": 6, "damaged": False},]before = [pod.copy() for pod in original_pods]choose_pod(original_pods)assert original_pods == before
A sorted approach can also preserve the input if it creates a new list:
Calling original_pods.sort(...) would reorder the caller’s list. Unit 6 taught why that difference matters; the current contract tells you which operation is allowed.
The running-best version has another advantage: its state directly represents the result being built. Lesson 4 will compare its cost with alternatives.
Under the original oxygen-first rule, WIDE-8 wins. Under pure oxygen-per-mass efficiency, LIGHT-6 wins because 6 / 3 is greater than 8 / 8. If the intended new rule still prioritizes oxygen before efficiency, the winner would not change. The ambiguity must be resolved before code.
format the result as "Next stage: <name>" or "No performance available".
Complete these stages:
solve the supplied input by hand;
write verb-first pseudocode;
draw a trace table with current record and running choice;
define is_available, is_better_performance, choose_performance, and format_performance contracts;
implement one usable record and no-result behavior;
add main comparison, tie, and cancellation rules one at a time;
run at least six progressive assertions;
prove the input remains equal to a preserved copy; and
change the tie rule and update every affected layer.
Hint: make comparison independent
Let choose_performance own traversal and eligibility. Put only the question “should this usable candidate replace the usable current choice?” inside is_better_performance.
Show a complete strategy after attempting the lab
Initialize the running choice to None. Skip cancelled records. Replace None with the first available record. For later available records, replace when the rating is greater or when ratings tie and duration is shorter. Do not replace an exact tie. Return None or the chosen name, then pass that value to a separate formatting function. Check cancellation with a record that would otherwise win.
14. Explain the route from input to message
Use the completed supply selector or festival planner and answer aloud:
Which function reads the collection?
Which function owns each decision?
What value persists between iterations?
Which line changes that value?
Why does an exact tie keep the earlier item?
Which function returns data, and which edge displays it?
Which check would fail first if eligibility were reversed?