Concepts / Container iteration with range-based for loops

Container iteration with range-based for loops

find_all() finds all occurrences of a specified character in a container and returns a range of iterators pointing to them.

  • Programming
Interactive lab

Try it: What a for Loop Hands You

What a Python for loop gives its loop variable from each kind of iterable: the characters of a string, the positions from range(len(word)), the elements of a list, (index, item) pairs from enumerate(), the numbers of range(start, stop, step), a dict's keys, its .values() and its .items() tuples unpacked into key, val, the inner lists of a nested list and the values an inner loop visits; and why removing items from a list while looping over it skips the next item, while looping over a copy nums[:] does not.

How it works

  1. The for statement asks the object for an iterator, then calls next() on it once per pass.
  2. Each next() result is bound to the loop variable (a tuple result such as ('a', 10) or (0, 'red') is unpacked into two names), and the indented body runs once with it.
  3. A string gives characters, a list gives element values (not positions), range() computes start, start + step, … stopping before stop, and a dict gives its keys in insertion order (.values() the values, .items() key-value tuples).
  4. A nested list's inner list is one element of the outer loop; a second, inner for loop is needed to visit the values inside it.
  5. A list iterator only remembers a position number into the live list: removing an item shifts the later items left, so the item that slides into the passed position is skipped. Looping over a copy (nums[:]) keeps the positions fixed.
  6. When next() has nothing left the loop ends (StopIteration) and the loop variable keeps its last value; an empty iterable runs the body zero times.

Default run (10 steps): A 3-line program. Step through it: each pass of the for loop asks the iterator for its next item, binds the loop variable to it and runs the indented body once. … Done: the loop body ran 3 times. Output: red | green | blue.

Simplified: The programs are twelve fixed templates: you choose the loop and edit only its data (a short string, list items that are words or integers, key: value pairs with word keys and integer values, range numbers from -20 to 20, up to 4 rows of integers, up to 10 numbers). Nothing you type is executed: the lab's own small parsers turn it into literals, and a model of CPython's iterator protocol written for these loops runs the program. Every program was also run in CPython 3.12 under sys.settrace, and the lab reproduces the variables before every line, the printed output, the final variables and the error.

Educational simulation

Loading the simulation…

Why a Quiet Test Can Still Be Informative

A function is only trustworthy once something checks its behavior. A small test function can exercise a search routine such as find_all() on a string, and it can also check every result that comes back, reporting a problem when one of those results is wrong. Following that check through the code is one of the fastest ways to understand what find_all() is supposed to deliver and how a test would reveal a mismatch.

Two mechanisms work together in this kind of test. The first is find_all(), which finds all occurrences of a specified character in a container and returns a range of iterators pointing to the characters it found. The second is the range-based for loop, which can walk through that range one iterator at a time. The rest of this article takes each mechanism in turn and then shows how they combine.

What find_all() Hands Back

The purpose of find_all() is to locate every occurrence of one specified character inside a container. In the test described by the source, the container is a string, which is simply a sequence of characters. The call find_all(m, 'a') searches the string m for the character a. The important point is the shape of what comes back: not a copy of the matching characters, and not a count, but a range of iterators. Each iterator points to one of the characters that was found.

A useful way to picture the result is as a set of pointers laid over the original string. Each returned iterator marks one position in the string where an a was found. The range is bounded by a begin and an end, so the pair together delimits exactly the results that find_all() produced. The diagram below shows this for the string Mary had a little lamb, where the search for a finds four matches.

How the Range-Based Loop Walks the Range

A range-based for loop visits each returned iterator in turn. On every pass, the loop variable receives the next iterator from the range, and in the test described by the source that variable is named p. Because p refers to the current found character, the loop body can look at that one character without needing to know anything about the rest of the results. When the iterators in the range are used up, the loop finishes.

The sequence below traces the four passes for Mary had a little lamb. On each pass, p points to one of the a characters, and the loop body runs once for it. The loop is driven entirely by the range, so the number of passes equals the number of matches that find_all() returned.

Tracing find_all on Mary had a little lamb

What do you think happens?

Read the test once more. Will the error message be written for the string Mary had a little lamb?

  • Yes, the message is written

  • No, the message is not written

Reveal answer
Answer:

No, the message is not written

find_all(m, 'a') returns only positions holding the character a. The if statement compares each character at those positions with a, so every comparison says the character is a. The message is written only when the comparison says the character is not a, which never happens for these results.

Worked example

Following the test from start to finish

The container m holds the string Mary had a little lamb. The call find_all(m, 'a') produces a range, and a range-based loop assigns each iterator to p. The if statement compares the character at p with a and writes a message to the error stream only when they differ. Trace what the test does.

  1. 1
    Locate the matches

    Counting positions from 0, the letter a appears at index 1 in Mary, at index 6 in had, at index 9 as the single word a, and at index 19 in lamb. find_all() therefore returns a range of four iterators.

  2. 2
    First pass

    The loop assigns the first iterator to p. p refers to the a at index 1. The if statement compares that character with a, finds them equal, and writes nothing.

  3. 3
    Middle passes

    The loop advances p to the a at index 6, then to the a at index 9. Each time the character at p is a, the comparison succeeds and no message is written.

  4. 4
    Final pass

    p refers to the a at index 19. The comparison again succeeds. Because this was the last iterator in the range, the loop finishes.

  5. 5
    Outcome

    Every returned character matched the expected value, so the error stream received nothing. Silence is the expected result for this input.

Result

Four passes run, the comparison succeeds on each, and no message is written. The test reports no failure.

Where a Failure Would Be Detected

The test is written to be quiet on success and loud on failure. The check that decides between the two is the if statement inside the loop body. That is the line where the expected value, the character a, meets the actual value, the character at p. When those two disagree, the comparison reports a mismatch and the message is written to the error stream.

For this test to report a failure, the loop would have to reach a pass in which p refers to a character that is not a. Given how find_all() is used here, that would only happen if the search handed back a position whose character was not the one requested. The flowchart below shows the decision on each pass. The source describes the message being written at the failing comparison, and it does not specify what happens to the loop after that, so the diagram stops at the reporting step rather than guessing at the next move.

Mistakes When Reading a Silent Test

  • Common mistake

    Assuming that no output means the test did nothing

    Why it fails

    In this function, silence means the loop ran to completion without finding a returned character that was not an a. The loop still made every pass and performed every comparison.

    Do this instead

    Read the silence as a success result for this input, and remember that the check ran on every iterator in the range.

  • Common mistake

    Thinking find_all() returns the matching letters as a new string

    Why it fails

    find_all() returns a range of iterators, and each one points to a position in the original string m.

    Do this instead

    Picture each element as a pointer back into the container, not as a copy of the character.

  • Common mistake

    Searching the whole message for a failure instead of the comparison

    Why it fails

    A failure is reported by the if statement inside the loop body, which is where the expected value meets the actual one.

    Do this instead

    When a failure appears, go to the comparison in the loop body and ask which value of p first stopped matching.

  • Common mistake

    Forgetting that p changes on every pass, so reading one value of p as representative of all of them

Predicting Behavior on a New String

EASY

The same test is run on the string banana, still searching for the character a. Before you trace it, predict two things: how many iterators find_all() will return, and whether the error message will be written. Then trace each pass, checking the character at p on every iteration.

Hints
  • Count every position in banana that holds the letter a, including the ones at the start and end of the word.
  • Ask whether any iterator in the range can point to a character other than a, since only that would trigger the if statement's message.
  • Compare your count of passes with the number of iterators you counted in the first hint.

Key Takeaways

  • find_all() finds all occurrences of a specified character in a container and returns a range of iterators pointing to those characters, not copies of them.
  • A range-based for loop visits each returned iterator in turn, and the loop variable p refers to the current found character on each pass.
  • The test compares each found character with the expected value and writes a message only when the comparison fails, so silence means every match was the expected character.
  • When a failure is reported, locate the comparison inside the loop body, because that is where the expected value meets the actual one and where the failure is detected.

Related concepts

More concepts starting with C