FreeCampus Python

Visiting Items in a Collection

Trace what simple for loops receive from strings, lists, tuples, dictionaries, sets, ranges, enumerate, and zip while preserving order and alignment.
python-foundations collections-iteration traversal range enumerate zip
Open in Colab
  • Level: Python Foundations · Unit 3
  • Estimated time: 3–4.5 hours
  • You will learn: Trace each value supplied during simple collection traversal and use range(), enumerate(), and zip() without losing position or alignment.
  • Practice in: Google Colab, JupyterLab, or a local editor

1. Visit every route stop without manual repetition

A route manifest contains stops in travel order:

route = ["dock", "garden", "tower", "vault"]

for stop in route:
    print(f"Scanning {stop}")

The for statement asks route to supply items one at a time. Before each indented execution, Python binds the next item to stop. The list itself remains unchanged.

You could write four separate print() calls, but the repeated structure would break as soon as the route changed. Traversal connects one stated action to every currently supplied item.

An iterable is a value that can supply a sequence of items for this kind of visit. This lesson focuses on what each built-in collection supplies. Unit 5 later opens the iterator protocol and explains generators.

The practical questions are:

  • What value does the loop target receive from each collection family?
  • When do positions matter enough to use range() or enumerate()?
  • How does zip() preserve alignment, and how can unequal lengths hide data?
  • Why can structural mutation make an active traversal invalid or misleading?

A for loop repeatedly asks an iterable for a next item, binds the target name, and executes the body until no items remain.

flowchart LR
    A["iterable"] --> B["next supplied item"]
    B --> C["bind loop target"]
    C --> D["run indented body"]
    D --> A
    A --> E["no items remain"]

2. Sequences supply values in sequence order

Lists, tuples, and strings preserve a deliberate order:

items = ["map", "rope"]
coordinate = (12, 7)
code = "NOVA"

for item in items:
    print(item)

for number in coordinate:
    print(number)

for character in code:
    print(character)

The three loop targets receive strings, integers, and one-character strings, respectively. The target name is ordinary: after traversal it remains bound to the last supplied value.

for character in "NOVA":
    print(character)

print(f"Last character: {character}")

That behavior is sometimes useful for tracing but usually should not be the way later code obtains the final item. code[-1] states that position directly.

An empty iterable runs the body zero times:

empty_route = []

for stop in empty_route:
    print("This line does not run")

print("Traversal finished")

If stop had not existed before, using it after this empty loop would raise NameError. Do not rely on a loop target as the only source of a result that must exist for empty input.

Rebinding the target does not replace a sequence item

names = ["nova", "mira"]

for name in names:
    name = name.title()

print(names)

The list still contains lowercase strings. Assignment changes which string the local name name references; it does not assign through a list position.

3. Dictionaries can supply keys, values, or pairs

Direct traversal of a dictionary supplies its keys in insertion order:

energy_by_pilot = {"Nova": 80, "Mira": 65, "Sol": 90}

for pilot in energy_by_pilot:
    print(pilot)

Use the chosen key to retrieve a value:

for pilot in energy_by_pilot:
    energy = energy_by_pilot[pilot]
    print(f"{pilot}: {energy}")

Or ask a view to supply exactly what the body needs:

for energy in energy_by_pilot.values():
    print(energy)

for pilot, energy in energy_by_pilot.items():
    print(f"{pilot}: {energy}")

Each .items() value is a two-item tuple. Tuple unpacking assigns its key to pilot and value to energy before running the body.

pairs = list(energy_by_pilot.items())
print(pairs)

This makes the supplied shape visible.

4. Sets supply members without a positional promise

skills = {"mapping", "repair", "translation"}

for skill in skills:
    print(skill)

Every current member is supplied once. The display order is not a stable program contract, so this would be fragile:

observed_order = []

for skill in skills:
    observed_order.append(skill)

print(observed_order)

The traversal is valid, but comparing observed_order with one hard-coded order is not. Sort for presentation or preserve an ordered source list when sequence meaning matters.

Checkpoint: what each collection supplies

5. range() supplies an arithmetic progression

range(stop) starts at zero and excludes the stop:

for index in range(4):
    print(index)

The values are 0, 1, 2, and 3. This aligns naturally with four list indexes, although direct item traversal is clearer when the body needs only each item.

range(start, stop) chooses the beginning:

print(list(range(2, 6)))

The small inspection list is [2, 3, 4, 5].

range(start, stop, step) chooses the distance and direction:

evens = range(2, 11, 2)
countdown = range(5, 0, -1)

print(list(evens))
print(list(countdown))

The stop remains excluded. A negative step needs a start greater than the stop. Bounds pointing in the wrong direction produce an empty range:

assert list(range(2, 8, -1)) == []
assert list(range(8, 2, 1)) == []

A zero step cannot make progress:

invalid = range(1, 5, 0)

Python raises ValueError: range() arg 3 must not be zero.

A range is not first built as a large list

positions = range(1_000_000)

print(type(positions).__name__)
print(len(positions))
print(positions[999_999])

The range stores its boundary rules compactly and calculates members as needed. Use list(range(...)) to inspect only small progressions; converting a million positions defeats that advantage.

6. enumerate() adds positions to values

Manual bookkeeping creates two state changes to keep synchronized:

route = ["dock", "garden", "vault"]
index = 0

for stop in route:
    print(index, stop)
    index = index + 1

enumerate() supplies (position, value) tuples directly:

for index, stop in enumerate(route):
    print(index, stop)

Choose a custom start for human-facing numbering:

for stop_number, stop in enumerate(route, start=1):
    print(f"Stop {stop_number}: {stop}")

The list indexes have not changed; start=1 changes only the numbers supplied by enumerate(). Do not use stop_number to index the list unless you account for that offset.

Inspect the shape:

numbered = list(enumerate(["dock", "vault"], start=1))
assert numbered == [(1, "dock"), (2, "vault")]

Checkpoint: range and enumerate

7. zip() aligns values by traversal position

Two collections may describe the same stops:

stops = ["dock", "garden", "vault"]
distances = [0, 4, 9]

for stop, distance in zip(stops, distances):
    print(f"{stop}: {distance} km")

zip() asks each input for one next item and supplies the grouped tuple. The first stop aligns with the first distance, then the second with the second.

pairs = list(zip(stops, distances))
assert pairs == [("dock", 0), ("garden", 4), ("vault", 9)]

More than two iterables can be aligned:

statuses = ["clear", "caution", "locked"]
rows = list(zip(stops, distances, statuses, strict=True))

assert rows[1] == ("garden", 4, "caution")

Default zip stops at the shortest input

stops = ["dock", "garden", "vault"]
distances = [0, 4]

pairs = list(zip(stops, distances))
print(pairs)

The unpaired "vault" disappears from the result without an error. That can be useful when shortest-input behavior is intended, but dangerous when unequal lengths mean corrupted alignment.

Use strictness for equal-length data contracts:

pairs = list(zip(stops, distances, strict=True))

Python raises ValueError when one iterable ends before the others.

Prefer records when fields belong together

Parallel lists are easy to misalign:

stops = ["dock", "garden", "vault"]
distances = [0, 4, 9]
statuses = ["clear", "caution", "locked"]

A list of complete records keeps each relationship local:

route_records = [
    {"stop": "dock", "distance": 0, "status": "clear"},
    {"stop": "garden", "distance": 4, "status": "caution"},
    {"stop": "vault", "distance": 9, "status": "locked"},
]

Use zip(strict=True) when parallel inputs arrive separately and equal length is a contract. Prefer records when the program owns the model and the values always travel together.

8. Do not invalidate an active traversal

Removing list items while visiting the same list can skip values because later items shift left:

stops = ["dock", "closed", "closed", "vault"]

for stop in stops:
    if stop == "closed":
        stops.remove(stop)

print(stops)

One "closed" may remain. After the first removal, the second shifts into the position the loop has already passed.

When intentional removal is small and already decided, traverse an outer copy:

stops = ["dock", "closed", "closed", "vault"]

for stop in stops.copy():
    if stop == "closed":
        stops.remove(stop)

assert stops == ["dock", "vault"]

Unit 4 develops clearer filtering patterns that build a new list rather than repeatedly removing.

Changing a dictionary’s size during traversal raises a direct error:

energy = {"Nova": 80, "Mira": 0}

for pilot in energy:
    if energy[pilot] == 0:
        del energy[pilot]

Python raises RuntimeError: dictionary changed size during iteration. Traverse a snapshot such as list(energy) for a deliberate small removal, or plan a separate result. Updating an existing value without changing dictionary size is permitted, but mixing traversal and mutation can still obscure the state transition.

Checkpoint: zip, alignment, and mutation

9. Assemble the expedition route manifest

Build predictable display lines from related collections. Use the named helpers instead of manual index bookkeeping.

stops = ["dock", "garden", "tower", "vault"]
distances = [0, 4, 7, 11]
status_by_stop = {
    "dock": "clear",
    "garden": "caution",
    "tower": "clear",
    "vault": "locked",
}

numbered_stops = None
aligned_route = None
manifest_lines = []
status_lines = []
inspection_positions = None

Complete these stages:

  1. make numbered_stops a list of (1, stop) tuples using enumerate();
  2. make aligned_route a list of stop-distance tuples with strict zip;
  3. traverse the aligned tuples with human-facing numbers and append exactly the required manifest lines;
  4. traverse dictionary items and append status lines in insertion order; and
  5. create odd zero-based inspection positions below the route length with one range().
assert numbered_stops == [
    (1, "dock"),
    (2, "garden"),
    (3, "tower"),
    (4, "vault"),
]
assert aligned_route == [
    ("dock", 0),
    ("garden", 4),
    ("tower", 7),
    ("vault", 11),
]
assert manifest_lines == [
    "Stop 1: dock (0 km)",
    "Stop 2: garden (4 km)",
    "Stop 3: tower (7 km)",
    "Stop 4: vault (11 km)",
]
assert status_lines == [
    "dock=clear",
    "garden=caution",
    "tower=clear",
    "vault=locked",
]
assert inspection_positions == [1, 3]
assert stops == ["dock", "garden", "tower", "vault"]
assert distances == [0, 4, 7, 11]

Boundary variation: remove the last distance and verify that strict zip fails. Restore the data, then add a complete fifth stop and distance and update the status mapping. A clean rerun should include it without another hard-coded append.

Hint: materialize only the values the assertions ask to retain

Use list(enumerate(stops, start=1)) and list(zip(stops, distances, strict=True)). A second enumerate(..., start=1) can number the already aligned (stop, distance) tuples. Dictionary .items() supplies status pairs. range(1, len(stops), 2) supplies odd indexes.

Show one complete solution after attempting the lab
numbered_stops = list(enumerate(stops, start=1))
aligned_route = list(zip(stops, distances, strict=True))

manifest_lines = []
for stop_number, route_pair in enumerate(aligned_route, start=1):
    stop, distance = route_pair
    manifest_lines.append(f"Stop {stop_number}: {stop} ({distance} km)")

status_lines = []
for stop, status in status_by_stop.items():
    status_lines.append(f"{stop}={status}")

inspection_positions = list(range(1, len(stops), 2))

The first two results are retained because the contract asks for them. The output lists are built separately, so none of the source collections change.

10. Keep the boundary with Unit 4 clear

This lesson establishes what an iterable supplies and how values align. Unit 4 will build the fuller control-flow toolkit:

  • choosing different actions with if, elif, and else;
  • accumulating totals and filtered results;
  • break, continue, and loop else;
  • nested traversal and grid algorithms;
  • condition-controlled while loops; and
  • comprehensions.

You do not need those techniques to explain the collection contracts on this page.

Key points

TipKey points
  • A simple for loop binds each item supplied by an iterable and runs its body.
  • Sequences supply values in order; dictionaries directly supply keys; sets supply every current member without a positional-order promise.
  • range() represents an excluded-stop arithmetic progression without first building a complete list.
  • enumerate() supplies (counter, value) tuples and can start its counter at a custom number.
  • zip() aligns inputs by traversal position; default zip truncates to the shortest input, while strict=True enforces equal exhaustion.
  • Keep related fields in records when parallel positions are fragile.
  • Avoid changing collection size while traversing that same structure.

References

Back to top