1.2.18 · D5Introduction to Programming (Python)

Question bank — for loop — iterating over sequences

1,558 words7 min readBack to topic

True or false — justify

Iterating a string with for ch in "cat": gives you whole words.
False — a str is a sequence of characters, so you get 'c', 'a', 't' one at a time; there are no word boundaries unless you .split() first.
for k in some_dict: loops over the dict's values.
False — plain iteration yields the keys. Use .values() for values or .items() for (key, value) pairs.
A for loop must know the number of items before it starts.
False — it never counts ahead; it just calls next() until StopIteration, so it works even on lazy/infinite-looking iterables like range or generators.
After for i in range(3): pass, the name i no longer exists.
False — in Python the loop variable leaks into the surrounding scope and keeps its last value, so i is 2.
range(1, 5) includes both 1 and 5.
False — range is half-open [1, 5), so it yields 1, 2, 3, 4; the stop value 5 is excluded.
zip(a, b) raises an error if a and b have different lengths.
False — it silently stops at the shorter sequence; the extra items of the longer one are simply ignored.
enumerate(seq) changes the items of seq into numbers.
False — it pairs each item with a running index, yielding (index, item) tuples; the original items are untouched.
Every for loop can be rewritten as a while loop plus iter/next.
True — that desugaring is the definition of for; it calls iter(seq) once, then next() in a loop until StopIteration triggers a break.
The loop variable is a fresh copy, so rebinding it inside the body affects the sequence.
False — the loop variable is just a name pointing at the current item; rebinding the name (e.g. x = 0) does not write back into the sequence.
Two nested for loops over the same range always run the same number of body executions.
True — the inner loop restarts fully on every outer pass, so total runs = (outer count) × (inner count), a fixed product for fixed ranges.

Spot the error

nums = [1, 2, 3, 4]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)
What goes wrong, and why?
Removing while iterating shifts later items left, but the iterator's position keeps advancing — so it skips the item that slid into the vacated slot (here 4 survives). Loop over a copy nums[:] or build a new list.
for i in range(len(items)):
    print(items[i])
Why is this considered un-Pythonic even though it works?
You reintroduced the very index bookkeeping the for loop exists to remove; for item in items: is clearer and can't produce an off-by-one or IndexError.
for x in seq:
    x = x * 2
print(seq)
Why is seq unchanged?
x = x * 2 only rebinds the local name x to a new value; it never reaches back into seq. To change the list you must assign by index or build a new list.
d = {"a": 1, "b": 2}
for k, v in d:
    print(k, v)
Why does this raise an error?
Iterating d yields single keys, not pairs, so Python can't unpack one string into k, v. Use for k, v in d.items():.
for i in range(5, 1):
    print(i)
Why does the loop body never run?
With a default step of +1, range(5, 1) can never move from 5 up to below 1, so it is empty — the loop body executes zero times (no error).
total = 0
for n in nums
    total += n
What's the syntax problem?
The for header must end with a colon :. Without it Python can't tell where the header stops and the indented body begins.

Why questions

Why does for prefer signalling "done" with an exception (StopIteration) instead of a length check?
Because it works uniformly on things without a known length (files, generators, network streams); asking "any more?" via next() is more general than "how many total?".
Why is the loop variable said to always move forward, never restart mid-loop?
The iterator is created once (a single iter() call) and only ever advances; nothing in the loop resets its cursor, so each item is delivered exactly once in order.
Why is range described as "lazy"?
It does not build a list of all numbers up front; it computes each value on demand as next() is called, so range(10**9) costs almost no memory.
Why does looping a dictionary give keys rather than values by design?
Keys are the dict's primary index — the thing you look items up by — so iterating them lets you reach any value via d[k] while also being the natural "identity" of each entry.
Why does enumerate exist when you could keep your own counter with i += 1?
A manual counter is an extra line that can drift out of sync or be forgotten; enumerate binds the correct index atomically with the item, so they can never disagree.
Why does tuple-unpacking for i, v in enumerate(seq): work at all?
enumerate yields a 2-element tuple each pass, and Python's assignment splits that tuple into the two names — the same mechanism as a, b = (1, 2).

Edge cases

Looping over an empty list for x in []: — how many times does the body run?
Zero times — the iterator is exhausted immediately, so StopIteration fires on the very first next() and the loop ends cleanly without error.
range(0) in a loop — what happens?
The range is empty ([0, 0) contains nothing), so the body runs zero times; this is the safe "do nothing" case, not an error.
After a loop over an empty sequence, is the loop variable defined?
No — since the body never ran, no assignment to the loop variable ever happened, so referencing it afterwards raises NameError.
Iterating a single-character string for ch in "x": — one item or none?
Exactly one iteration, binding ch = 'x'; a string of length 1 is still a sequence with one element, not an empty one.
zip() where one input is empty, e.g. zip([1, 2], []) — what does the loop do?
The shorter side has length 0, so zip stops before the first pair; the loop body runs zero times.
for i in range(3, 3): — included or empty?
Empty — with start equal to stop and a positive step there is no value in the half-open interval [3, 3), so the body never runs.
An iterator that was already fully consumed, looped again — what happens?
Nothing runs — an exhausted iterator keeps raising StopIteration, so a second loop over the same iterator object is empty (unlike a list, which can be re-iterated freshly).

Recall One-line summary of the traps

Most traps come from three habits: assuming stop is included (it isn't), assuming the loop variable is scoped/reset (it leaks and persists), and mutating the thing you iterate (the cursor drifts). Loop the data, don't fight it.


Connections

  • for loop — iterating over sequences — the parent this bank drills.
  • range function — source of the half-open [start, stop) traps.
  • Lists and indexing — the mutate-while-looping hazard lives here.
  • Strings as sequences — why looping a string yields characters.
  • Dictionaries — keys, values, items — keys-vs-values trap.
  • enumerate and zip — pairing and short-stopping behaviour.
  • Iterators and generators — why exhausted iterators stay empty.
  • while loop — condition-based repetition — the desugaring behind every for.