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 vikrange
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 1range 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 integerszip — 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 droppedThat 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 survivedBuild 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 breakingfor ... 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) # 4Digit 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:
breakConditionals
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(...)). Useenumeratewhen you need the index. rangeis lazy;range(10 ** 9)is free,list(range(10 ** 9))is not.zipstops at the shortest input, silently —strict=Truemakes it complain.zip(*grid)transposes a matrix.- Never remove items from a list you are iterating over; build a new list.
for ... elseruns theelsewhen the loop was not broken out of.- Comparisons chain:
0 <= i < nis one expression.