Lesson 06 · 9 min read · Python for placements

Loops that read like the problem statement

range, enumerate, zip, reversed and for-else — and why you should almost never write a C-style index loop in Python, plus what to do when you must.

Python's loops are built to iterate over things, not over indices. Code that fights this reads like a translation, and interviewers notice.

for iterates over a sequence, not a counter

names = ["raja", "anu", "vik"]

# what people write first, coming from C or Java
for i in range(len(names)):
    print(names[i])

# what Python wants
for name in names:
    print(name)

The second version cannot go out of bounds and cannot be off by one. Reach for range(len(...)) only when you genuinely need the index and nothing else will do.

When you need both the index and the value, that is what enumerate is for:

for i, name in enumerate(names):
    print(i, name)              # 0 raja / 1 anu / 2 vik

for rank, name in enumerate(names, start=1):
    print(rank, name)           # 1 raja / 2 anu / 3 vik

range

range(5)            # 0 1 2 3 4
range(2, 5)         # 2 3 4
range(0, 10, 2)     # 0 2 4 6 8
range(5, 0, -1)     # 5 4 3 2 1

range is lazy — it does not build a list. range(10 ** 9) costs nothing and uses a few bytes, because it stores start, stop and step and computes values on demand:

r = range(10 ** 9)
print(len(r))        # 1000000000, instantly
print(r[500])        # 500
print(list(r))       # do NOT — this would try to build a billion integers

zip — walk two sequences together

names = ["raja", "anu", "vik"]
marks = [87, 92, 78]

for name, mark in zip(names, marks):
    print(name, mark)

zip stops at the shortest input, silently:

print(list(zip([1, 2, 3], ["a", "b"])))    # [(1, 'a'), (2, 'b')] — the 3 is dropped

That silence is occasionally a bug. If the inputs must be the same length, say so:

list(zip([1, 2, 3], ["a", "b"], strict=True))   # ValueError, Python 3.10+

zip is also how you transpose a matrix, which looks like a magic trick until you see it once:

grid = [[1, 2, 3],
        [4, 5, 6]]
print([list(row) for row in zip(*grid)])
# [[1, 4], [2, 5], [3, 6]]

*grid unpacks the rows as separate arguments, so zip receives [1,2,3] and [4,5,6] and pairs them element-wise.

reversed, and iterating backwards

for x in reversed([1, 2, 3]):
    print(x)                    # 3 2 1

for i in range(len(xs) - 1, -1, -1):    # index-based, when you need i
    print(i, xs[i])

reversed() returns a lazy iterator and does not copy; xs[::-1] returns a new reversed list and does copy. Prefer reversed in a loop.

Modifying a list while looping over it

Do not. The iterator tracks a position, and removing an item shifts everything after it:

xs = [1, 2, 3, 4]
for x in xs:
    if x % 2 == 0:
        xs.remove(x)
print(xs)             # [1, 3] — looks right by luck

xs = [1, 2, 2, 3]
for x in xs:
    if x == 2:
        xs.remove(x)
print(xs)             # [1, 2, 3] — one 2 survived

Build a new list instead, which is clearer and correct:

xs = [1, 2, 2, 3]
xs = [x for x in xs if x != 2]      # [1, 3]

If you must edit in place, iterate over a copy — for x in xs[:] — and be explicit about why.

break, continue, and the for ... else nobody uses

break leaves the loop; continue skips to the next iteration. The unusual one is else on a loop, which runs only if the loop was not broken out of:

def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True

# the same shape with for/else, no early return
def first_factor(n):
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return i
    else:
        return None       # loop finished without breaking

for ... else reads as "search, and if you never found it, do this". It replaces the found = False flag variable:

for row in table:
    if row.id == target:
        print("found")
        break
else:
    print("not in the table")

Knowing it exists is a small signal that you have read Python rather than transliterated another language into it. Note the trap: else runs when the loop completes, including when the sequence was empty.

while, and when it is right

Use while when the number of iterations is not known in advance:

n = 1234
digits = 0
while n > 0:
    n //= 10
    digits += 1
print(digits)      # 4

Digit extraction, binary search, two pointers converging, "keep going until stable" — those are while problems. Anything with a known collection is a for problem.

Python has no do ... while. The idiom is an infinite loop with the condition at the end:

while True:
    value = read_next()
    process(value)
    if value is None:
        break

Conditionals

elif, no switch until 3.10, and no ternary ? : — Python spells it out:

status = "pass" if marks >= 40 else "fail"

Comparisons chain, which is unusual and genuinely nice:

if 0 <= index < len(xs):        # one expression, evaluated once each
    ...
if 40 <= marks <= 100:
    ...

From Python 3.10 there is match, which is pattern matching rather than a C switch:

match command.split():
    case ["go", direction]:
        move(direction)
    case ["quit"]:
        stop()
    case _:
        print("unknown")

It destructures as it matches. You will rarely need it in a coding round, but if you mention it, be ready to say it is not a switch — it binds names from the shape of the data.

What to take into an interview

  • Iterate over the sequence, not over range(len(...)). Use enumerate when you need the index.
  • range is lazy; range(10 ** 9) is free, list(range(10 ** 9)) is not.
  • zip stops at the shortest input, silently — strict=True makes it complain.
  • zip(*grid) transposes a matrix.
  • Never remove items from a list you are iterating over; build a new list.
  • for ... else runs the else when the loop was not broken out of.
  • Comparisons chain: 0 <= i < n is one expression.
Lesson 6 of 12 · all 12 are free to read, no account. Course contents