FreeCampus Python

Turn a Plan into Working Python

Move from a hand-solved case to traceable pseudocode, cohesive function contracts, and an incremental Python implementation with checks at every stage.
python-foundations problem-solving-algorithms decomposition pseudocode
Open in Colab
  • Level: Python Foundations
  • Estimated time: 3–4 hours
  • You will learn: Turn a manually solved case into pseudocode, cohesive functions, and a working program built one checkable behavior at a time.
  • Practice in: Google Colab, JupyterLab, or a local editor

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:

  1. What did you do by hand to solve one small case?
  2. Which value must survive while you inspect later records?
  3. Where can the work be split into independently checkable responsibilities?
  4. Does the pseudocode show every decision, update, and stopping point?
  5. What is the smallest end-to-end behavior you can make pass now?

1. Solve one supply case without Python

Use the rule set from Lesson 1:

  • ignore damaged pods;
  • prefer greater oxygen;
  • on an oxygen tie, prefer lower mass;
  • on an exact tie, keep the earlier pod;
  • return its ID, or None when no pod is usable;
  • do not modify the input.

Start with three records:

pods = [
    {"id": "LUNA-4", "oxygen": 8, "mass": 6, "damaged": False},
    {"id": "NOVA-2", "oxygen": 8, "mass": 4, "damaged": False},
    {"id": "DUST-9", "oxygen": 10, "mass": 3, "damaged": True},
]

Solve it on paper:

  1. LUNA-4 is usable, so it becomes the current choice.
  2. NOVA-2 is usable and ties on oxygen. Its lower mass replaces LUNA-4.
  3. DUST-9 has more oxygen but is damaged, so it cannot replace the choice.
  4. 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:

exact_tie_pods = [
    {"id": "LUNA-4", "oxygen": 8, "mass": 6, "damaged": False},
    {"id": "NOVA-2", "oxygen": 8, "mass": 6, "damaged": False},
]

exact_tie_expected = "LUNA-4"

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 result
FOR 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 pod
IF there is no best pod
    RETURN no result
RETURN 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:

damaged_first = [
    {"id": "DUST-9", "oxygen": 10, "mass": 3, "damaged": True},
    {"id": "NOVA-2", "oxygen": 8, "mass": 4, "damaged": False},
]

damaged_first_expected = "NOVA-2"

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."""
    return not 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."""
    pass


def 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.

Check the helpers before the whole selector:

assert is_usable(pods[0]) is True
assert is_usable(pods[2]) is False
assert is_better(pods[1], pods[0]) is True
assert is_better(pods[0], pods[1]) is False

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 = None
    for pod in pods:
        if not pod["damaged"]:
            if best is None or pod["oxygen"] > best["oxygen"]:
                best = pod
    if best is None:
        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 > right


def 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.

Checkpoint: hand traces and function boundaries

7. Separate calculation from display

A calculation function should return a result that another function can use:

def format_choice(pod_id):
    """Return a message describing the selection result."""
    if pod_id is None:
        return "No usable supply pod"
    return f"Dispatch {pod_id}"

Now formatting has direct checks:

assert format_choice("NOVA-2") == "Dispatch NOVA-2"
assert format_choice(None) == "No usable supply pod"

Printing can remain at the outer edge:

selected_id = "NOVA-2"
message = format_choice(selected_id)
print(message)

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.

8. Build the smallest complete path

Begin with the smallest successful input:

single_pod = [
    {"id": "SOLO-1", "oxygen": 5, "mass": 9, "damaged": False}
]

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 = None

    for pod in pods:
        if not is_usable(pod):
            continue
        if best is None:
            best = pod

    if best is None:
        return None
    return best["id"]

Run the narrow checks:

assert choose_pod(single_pod) == "SOLO-1"
assert choose_pod([]) is None

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 = None

    for pod in pods:
        if not is_usable(pod):
            continue
        if best is None or is_better(pod, best):
            best = pod

    if best is None:
        return None
    return best["id"]

Now run checks in an order that narrows failures:

assert choose_pod(single_pod) == "SOLO-1"
assert choose_pod([]) is None
assert choose_pod(damaged_first) == "NOVA-2"
assert choose_pod(pods) == "NOVA-2"
assert choose_pod(exact_tie_pods) == "LUNA-4"

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:

print(is_usable(pods[1]))
print(is_better(pods[1], pods[0]))
print(choose_pod(pods))
print(format_choice(choose_pod(pods)))

Expected observations are:

True
True
NOVA-2
Dispatch NOVA-2

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:

ordered = sorted(
    original_pods,
    key=lambda pod: (-pod["oxygen"], pod["mass"]),
)

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.

Checkpoint: incremental implementation

12. Carry a changed rule through every layer

Change the mass tie rule from “lower mass wins” to “higher oxygen efficiency wins,” where efficiency is oxygen / mass.

Do not begin by editing is_better. Update in this order:

  1. contract wording;
  2. a two-pod example whose winner changes;
  3. hand calculation;
  4. pseudocode comparison step;
  5. is_better implementation;
  6. progressive assertions; and
  7. explanation of the observed change.

One possible distinguishing case is:

changed_rule_pods = [
    {"id": "WIDE-8", "oxygen": 8, "mass": 8, "damaged": False},
    {"id": "LIGHT-6", "oxygen": 6, "mass": 3, "damaged": False},
]

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.

13. Lab: build a festival schedule planner

A small festival has performance records:

performances = [
    {"name": "Comet Choir", "rating": 8, "minutes": 45, "cancelled": False},
    {"name": "Solar Drums", "rating": 9, "minutes": 60, "cancelled": True},
    {"name": "Aurora Strings", "rating": 8, "minutes": 35, "cancelled": False},
]

Use this contract:

  • ignore cancelled performances;
  • prefer a greater rating;
  • on a rating tie, prefer the shorter performance;
  • on an exact tie, keep the earlier record;
  • return the selected name or None;
  • do not mutate the input;
  • format the result as "Next stage: <name>" or "No performance available".

Complete these stages:

  1. solve the supplied input by hand;
  2. write verb-first pseudocode;
  3. draw a trace table with current record and running choice;
  4. define is_available, is_better_performance, choose_performance, and format_performance contracts;
  5. implement one usable record and no-result behavior;
  6. add main comparison, tie, and cancellation rules one at a time;
  7. run at least six progressive assertions;
  8. prove the input remains equal to a preserved copy; and
  9. 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?
  • How do you know the input was not changed?

Checkpoint: a plan ready to adapt

Key points

  • Solve a small case manually to expose decisions, persistent state, and the expected result.
  • Pseudocode should show traversal, conditions, updates, stopping, and return behavior without depending on exact Python punctuation.
  • Split functions at meaningful rules and side-effect boundaries, not at every line.
  • Build a narrow end-to-end behavior, then add one acceptance rule at a time.
  • Inspect the earliest incorrect stage and preserve the caller’s ownership contract while implementing.

References

Back to top