FreeCampus Python

Searching, Skipping, and Stopping Loops

Design searches for first, last, all, or existence results; use break, continue, and loop else deliberately; and preserve an explicit exit reason.
python-foundations decisions-repetition loop-control
Open in Colab
  • Level: Python Foundations · Unit 4
  • Estimated time: 3.5–5 hours
  • You will learn: Define the search result before looping, skip unusable values, stop after sufficient evidence, and explain loop else and nearest-loop behavior.
  • Practice in: Google Colab, JupyterLab, or a local editor

1. Define what “find” means before searching

A signal scanner receives several readings:

signals = [
    {"code": "A1", "strength": 2},
    {"code": "B7", "strength": 9},
    {"code": "B7", "strength": 6},
]

“Find B7” is incomplete. The required artifact might be:

  • the first matching record;
  • the last matching record;
  • all matching records;
  • whether any match exists; or
  • the position of the first match.

Each contract implies different loop behavior. A first-match search can stop as soon as it has sufficient evidence:

first_match = None

for signal in signals:
    if signal["code"] == "B7":
        first_match = signal
        break

assert first_match == {"code": "B7", "strength": 9}

break exits the loop immediately. The final record is never inspected, which is correct only because later matches cannot change the first result.

Questions this lesson will answer

  • When should a search stop, and when must it inspect every item?
  • How does continue differ from break and pass?
  • What does a loop’s else really mean?
  • Which loop receives a break inside nested code?
  • When should membership or any() replace a manual search?

2. First, last, and all matches require different updates

A last-match search must keep traversing:

last_match = None

for signal in signals:
    if signal["code"] == "B7":
        last_match = signal

assert last_match == {"code": "B7", "strength": 6}

Every new match replaces the prior one. An all-match search collects instead:

all_matches = []

for signal in signals:
    if signal["code"] == "B7":
        all_matches.append(signal)

assert all_matches == [signals[1], signals[2]]

An existence search needs only a Boolean:

found = False

for signal in signals:
    if signal["code"] == "B7":
        found = True
        break

assert found is True

State the no-match result too: None for no selected record, [] for no collected records, and False for no existence evidence. Empty input should produce the same defined values without special repair after the loop.

4. Loop else means no break occurred

A loop may have an else aligned with it:

codes = ["A1", "C3", "B7"]
target = "B7"

for code in codes:
    if code == target:
        result = f"found {target}"
        break
else:
    result = f"{target} not found"

assert result == "found B7"

The else runs only if the loop finishes without break. It does not mean the final if condition was false. Several nonmatching iterations may occur, but a later break still skips the loop else.

For no match:

codes = ["A1", "C3"]
target = "B7"

for code in codes:
    if code == target:
        result = f"found {target}"
        break
else:
    result = f"{target} not found"

assert result == "B7 not found"

An empty collection also reaches else because no break occurred.

TipAttach the meaning to break

Read loop else as “normal exhaustion.” Use it when break has one clear meaning such as success. If several unrelated breaks exist, an explicit exit-reason name may be easier to understand.

5. A while loop can also have else

values = [3, 5, 8]
index = 0

while index < len(values):
    if values[index] % 2 == 0:
        first_even = values[index]
        break
    index += 1
else:
    first_even = None

assert first_even == 8

The else runs if the condition becomes false normally. A break skips it. As in Lesson 5, every path that stays in the loop must advance index; here the break path exits, and the nonmatch path increments.

6. Record why a loop exited

One scanner can encounter success, shutdown, exhaustion, or an invalid limit:

commands = ["noise", "target", "shutdown"]
exit_reason = "exhausted"
found = None

for command in commands:
    if command == "shutdown":
        exit_reason = "shutdown"
        break
    if command == "target":
        found = command
        exit_reason = "target found"
        break

assert exit_reason == "target found"
assert found == "target"

Initialize the normal-exhaustion reason before the loop; replace it immediately before each break. This leaves an explicit result after all paths.

A loop else would also work for exhaustion, but it would not distinguish two kinds of break by itself. Choose the form that makes exit states most visible.

Checkpoint: loop else and exit reasons

7. break exits only the nearest loop

Nested loops make the target important:

groups = [
    ["noise", "target", "noise"],
    ["target", "noise"],
]
visited = []

for group_number, group in enumerate(groups):
    for signal in group:
        visited.append((group_number, signal))
        if signal == "target":
            break

assert visited == [
    (0, "noise"),
    (0, "target"),
    (1, "target"),
]

Each break exits only the inner loop. The outer loop proceeds to the next group. If the whole search should stop, preserve a flag and check it in the outer loop:

found = None

for group_number, group in enumerate(groups):
    for signal in group:
        if signal == "target":
            found = (group_number, signal)
            break
    if found is not None:
        break

assert found == (0, "target")

Lesson 7 applies this behavior to grids.

8. pass does nothing; it does not skip the body

pass is a placeholder statement:

for value in [1, 2, 3]:
    if value == 2:
        pass
    print(value)

All three values print. For value 2, pass performs no action, then execution continues with the next statement in the same body.

With continue, the value 2 would not print:

for value in [1, 2, 3]:
    if value == 2:
        continue
    print(value)

Use pass only when Python requires a statement but the block is intentionally empty during development or by design. It is not a synonym for “skip this item.”

9. Prefer direct operations for direct questions

Manual loops are excellent for learning and for collecting rich evidence. When the final question is already a built-in operation, say it directly:

codes = ["A1", "C3", "B7"]

assert "B7" in codes
assert any(code.startswith("C") for code in codes)
assert all(len(code) == 2 for code in codes)
  • Membership answers whether an equal value exists.
  • any(...) answers whether at least one generated fact is truthy.
  • all(...) answers whether every generated fact is truthy.

The expressions inside any and all are generator expressions, a lazy form previewed here because the built-ins consume it directly. Lesson 8 focuses on comprehensions and explains why square brackets would unnecessarily build an intermediate list for this yes/no question.

Use an explicit loop when you need the matching record, position, rejection log, or exit reason—not only a Boolean.

Checkpoint: control statements and direct questions

10. Build a signal scanner

Use this ordered signal stream:

signals = [
    {"code": "", "strength": 8},
    {"strength": 9},
    {"code": "A1", "strength": 4},
    {"code": "B7", "strength": 3},
    {"code": "B7", "strength": 9},
    {"code": "SHUTDOWN", "strength": 0},
    {"code": "B7", "strength": 10},
]
target = "B7"
minimum_strength = 7

rejected = []
first_match = None
first_position = None
exit_reason = "exhausted"

Implement this contract:

  1. Reject a record when the code key is absent or the code is empty; preserve its position and reason, then continue.
  2. Stop with reason "shutdown" when code is "SHUTDOWN".
  3. A target qualifies only when strength is at least the minimum.
  4. Stop at the first qualifying target, preserving the record and its position.
  5. If traversal exhausts normally, keep reason "exhausted".
  6. Preserve the source list unchanged.

Run:

assert rejected == [
    (0, "empty code"),
    (1, "missing code"),
]
assert first_match == {"code": "B7", "strength": 9}
assert first_position == 4
assert exit_reason == "target found"
assert signals[-1]["strength"] == 10

Then test no target, shutdown before a target, an empty list, and a qualifying target in the first position. Explain whether loop else or the initialized exit reason better communicates your implementation.

Hint: order rejection, shutdown, and success checks

Use enumerate(signals) and make the missing-key check before indexing code. Give missing and empty codes distinct rejection tuples. Set exit_reason immediately before each break. A loop else can explicitly restore or confirm "exhausted".

Show one complete solution after attempting the lab
signals = [
    {"code": "", "strength": 8},
    {"strength": 9},
    {"code": "A1", "strength": 4},
    {"code": "B7", "strength": 3},
    {"code": "B7", "strength": 9},
    {"code": "SHUTDOWN", "strength": 0},
    {"code": "B7", "strength": 10},
]
target = "B7"
minimum_strength = 7

rejected = []
first_match = None
first_position = None
exit_reason = "exhausted"

for position, signal in enumerate(signals):
    if "code" not in signal:
        rejected.append((position, "missing code"))
        continue
    if not signal["code"]:
        rejected.append((position, "empty code"))
        continue
    if signal["code"] == "SHUTDOWN":
        exit_reason = "shutdown"
        break
    if signal["code"] == target and signal["strength"] >= minimum_strength:
        first_match = signal
        first_position = position
        exit_reason = "target found"
        break
else:
    exit_reason = "exhausted"

The two target records at positions 3 and 4 demonstrate that matching the code is not enough; the strength rule still decides qualification.

11. Explain the stopping decision

  1. Why can a first-match loop stop but an all-match loop cannot?
  2. What evidence should be recorded before continue discards the current path?
  3. Why does loop else mean no break rather than “the final if was false”?
  4. How would you stop both loops in a nested search?
  5. When do membership, any(), or all() express the entire question?

Key points

TipKey points
  • Define whether the result is first, last, all, existence, or position before choosing loop control.
  • break exits the nearest loop; continue skips the remaining current body; pass does nothing.
  • A loop’s else runs after normal exhaustion and is skipped by break.
  • Record rejection evidence before continuing and an exit reason before breaking when several exit states matter.
  • Use membership, any(), or all() when a direct Boolean is the whole result; keep an explicit loop for richer artifacts.

References

Back to top