Senior 10 min · March 05, 2026

Python for loop — Silent Data Loss from List Modification

47% of flagged records persisted after a for loop cleanup due to in-place list modification — avoid this with list comprehensions or iterating a copy.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • A for loop visits every item in an iterable and runs the same block of code for each — Python handles counting automatically
  • range(n) starts at 0 and excludes n — range(5) gives 0,1,2,3,4, never 5
  • enumerate() gives you both index and value — use it instead of a manual counter
  • zip() pairs items from two sequences together — use it instead of range(len()) when processing two lists in lockstep
  • break exits the loop; continue skips the current iteration; else runs only if no break occurred
  • Modifying a list while looping over it silently skips items — loop over a copy or build a new list
  • List comprehensions are faster and safer than a for loop that builds a new list — prefer them when the body is a single expression
  • Biggest trap: the for...else clause runs when the loop completes WITHOUT break, not when it breaks
Plain-English First

Imagine you have a bag of 10 apples and you want to check each one for bruises. You don't write a separate rule for apple 1, apple 2, apple 3 — you just say 'for every apple in the bag, do this check.' That's exactly what a for loop does in Python. It lets you write one instruction and automatically repeat it for every item in a collection, without you having to count how many items there are or manually keep track of where you are.

Every real program does repetitive work. A weather app checks temperatures for 365 days. A music app loads every song in your library. An online store applies a discount to every item in your cart.

The for loop solves this elegantly. You write an action once and Python automatically repeats it for every item in a collection — advancing through the sequence, tracking position, and stopping at the end, all without you doing anything extra.

I've reviewed a lot of Python code over the years — across data pipelines, API services, and scrappy internal tools — and for loop misuse shows up more than almost any other category of bug. Usually it's subtle: a list modified mid-iteration, a zip() silently swallowing records, a for...else clause that does the opposite of what the author intended. Most of these issues don't crash — they just produce wrong results quietly, which is worse.

By the end of this article you'll understand the for loop syntax properly, know how to iterate over lists, strings, dictionaries, ranges, and multiple sequences at once with zip(), use enumerate() the way Pythonistas actually use it, write list comprehensions instead of accumulator loops, and recognise the mistakes that trip up nearly every beginner — including a few that experienced developers still walk into on a bad day.

What a for Loop Actually Does — The Core Idea

A for loop says: 'take this collection of things, visit each one in order, and run the same block of code for each.' Python handles all the counting and moving-forward for you. You just describe what to DO on each visit.

The collection you loop over can be a list of names, a string of characters, a range of numbers — almost anything that holds multiple items. Python calls these 'iterables', but you can think of them as 'anything you can step through one piece at a time.'

The basic shape of a for loop is always the same: the word for, then a variable name you choose (this is your 'current item' holder), then in, then the collection, then a colon. Everything indented below that runs once per item. The indentation is not optional — Python uses it to know which lines belong inside the loop and which don't.

Beyond the syntax, it's worth understanding what Python is actually doing under the hood. When you write for planet in planets, Python calls iter(planets) to get an iterator object, then calls next() on it repeatedly until it raises a StopIteration exception — at which point the loop ends cleanly. You can do this manually yourself to see exactly what the loop sees step by step. This also explains why you can loop over strings, files, generators, and custom objects — anything that implements __iter__ and __next__ participates in the same protocol. That's not implementation detail trivia; it's the mental model that makes debugging iterator errors actually tractable.

first_for_loop.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# ── Basic for loop ───────────────────────────────────────────────────────────
planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter"]

for planet in planets:           # 'planet' holds the current item each round
    print(f"Visiting {planet}")  # this line runs once per planet

print("Tour complete!")          # NOT indented — runs after the loop finishes

# ── What Python actually does under the hood ─────────────────────────────────
# The for loop above is equivalent to this manual iterator protocol:
print("\n=== Manual iterator (what Python does internally) ===")
iterator = iter(planets)         # get an iterator from the list

while True:
    try:
        planet = next(iterator)  # ask for the next item
        print(f"Visiting {planet}")
    except StopIteration:        # no more items — loop ends cleanly
        break

# Knowing this explains WHY you can loop over strings, files, generators:
# any object that implements __iter__ and __next__ is fair game.
print("\n=== Strings are iterable too ===")
for char in "Mars":             # string is just a sequence of characters
    print(char)
Output
Visiting Mercury
Visiting Venus
Visiting Earth
Visiting Mars
Visiting Jupiter
Tour complete!
=== Manual iterator (what Python does internally) ===
Visiting Mercury
Visiting Venus
Visiting Earth
Visiting Mars
Visiting Jupiter
=== Strings are iterable too ===
M
a
r
s
The Iterable Mental Model
  • Python calls iter() on the collection to get an iterator object
  • Each iteration calls next() to get the next item — StopIteration signals the end and the loop exits cleanly
  • The loop variable is assigned fresh on each iteration — it does not carry state between rounds
  • Any object that implements __iter__ and __next__ can be looped over — not just lists
  • Indentation defines the loop body — no braces needed, Python enforces structure through layout
Production Insight
Python's for loop works on any iterable — not just lists.
Strings, tuples, dictionaries, files, generators, database query results, and network responses are all fair game.
The practical rule: if you can use 'in' with it in a membership test, you can almost certainly loop over it.
Understanding iter() and next() is what separates engineers who debug iterator errors confidently from engineers who just add print statements and hope.
Key Takeaway
A for loop visits every item in an iterable — Python handles the counting and advancing automatically.
The loop variable is assigned fresh each iteration — it holds the current item only.
Any object implementing __iter__ can be looped over — understanding iter() and next() makes debugging iterator errors straightforward instead of mysterious.
Choosing What to Loop Over
IfNeed to process every item in a list
UseUse for item in my_list — direct iteration is cleanest and most readable
IfNeed to repeat something N times without caring about the items
UseUse for _ in range(n) — underscore signals the variable is intentionally unused
IfNeed both index and value from a list
UseUse for i, item in enumerate(my_list) — not a manual counter variable
IfNeed to loop over dictionary keys and values together
UseUse for key, value in my_dict.items() — direct dict iteration only gives keys

Looping Over Numbers with range() — Your Most-Used Tool

Most beginners need to repeat something a set number of times — run a countdown from 10, process 100 items, print a multiplication table. For that, Python gives you range(). It generates a sequence of numbers on demand without storing them all in memory at once.

range(5) gives you 0, 1, 2, 3, 4 — five numbers, starting at zero. This trips up almost every beginner at least once: range counts from zero by default, and the number you pass in is NOT included in the output. Think of it as 'up to but not including 5.'

range(start, stop) lets you control where counting begins. range(1, 6) gives 1, 2, 3, 4, 5. range(start, stop, step) adds a step size — range(0, 10, 2) gives you every even number from 0 to 8. You can count backwards with a negative step: range(5, 0, -1) counts down from 5 to 1.

One thing worth internalising early: range() is a lazy object in Python 3. It does not generate all the numbers upfront and store them in a list. It calculates each number on demand as the loop requests it. range(1_000_000) uses a few dozen bytes of memory regardless of how large the range is. This is why you should never write list(range(n)) just to loop over numbers — you're forcing Python to materialise the entire sequence into memory for no reason. The range object already supports indexing, slicing, and membership testing without being converted to a list.

range_examples.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# ── range(n): repeat N times, starting from 0 ────────────────────────────────
print("=== Countdown prep ===")
for step_number in range(5):           # generates 0, 1, 2, 3, 4
    print(f"Step {step_number}")

# ── range(start, stop): control where counting begins ────────────────────────
print("\n=== Lap counter ===")
for lap in range(1, 6):                # generates 1, 2, 3, 4, 5 (NOT 6)
    print(f"Lap {lap} complete")

# ── range(start, stop, step): skip values ────────────────────────────────────
print("\n=== Even floors only ===")
for floor in range(0, 11, 2):          # 0, 2, 4, 6, 8, 10
    print(f"Elevator stops at floor {floor}")

# ── Counting backwards ────────────────────────────────────────────────────────
print("\n=== Rocket launch ===")
for countdown in range(5, 0, -1):      # 5, 4, 3, 2, 1
    print(f"T-minus {countdown}...")
print("Liftoff!")

# ── range() is lazy — not a list ─────────────────────────────────────────────
import sys

lazy_range   = range(1_000_000)
eager_list   = list(range(1_000_000))

print(f"\nrange(1_000_000) memory: {sys.getsizeof(lazy_range)} bytes")
print(f"list(range(1_000_000)) memory: {sys.getsizeof(eager_list):,} bytes")
# Never convert range to a list just to iterate — there is no benefit
# range already supports len(), indexing, and 'in' checks natively
Output
=== Countdown prep ===
Step 0
Step 1
Step 2
Step 3
Step 4
=== Lap counter ===
Lap 1 complete
Lap 2 complete
Lap 3 complete
Lap 4 complete
Lap 5 complete
=== Even floors only ===
Elevator stops at floor 0
Elevator stops at floor 2
Elevator stops at floor 4
Elevator stops at floor 6
Elevator stops at floor 8
Elevator stops at floor 10
=== Rocket launch ===
T-minus 5...
T-minus 4...
T-minus 3...
T-minus 2...
T-minus 1...
Liftoff!
range(1_000_000) memory: 48 bytes
list(range(1_000_000)) memory: 8,000,056 bytes
Watch Out: range(5) Starts at 0, Not 1
This is the single most common beginner mistake with range(), and it produces off-by-one errors that are genuinely annoying to track down. range(5) gives you [0, 1, 2, 3, 4] — five values, but none of them is 5. If you need to print 1 through 5, use range(1, 6). A good mental model: the stop value is a wall the counter hits but never crosses. The memory comparison in the code above also shows why you should never write list(range(n)) just to loop — range is already lazy and memory-efficient on its own, and it supports indexing and membership tests without being materialised.
Production Insight
range() is a lazy sequence object in Python 3 — it does not store all numbers in memory.
range(1_000_000) uses 48 bytes regardless of the stop value. list(range(1_000_000)) uses roughly 8MB.
The range object also supports len(), slicing, and 'in' membership checks without conversion — so there is almost never a reason to call list() on it.
Rule: always use range() directly for looping. Only convert to list if you genuinely need multiple independent passes or need to serialize the sequence.
Key Takeaway
range(n) starts at 0 and excludes n — it never includes the stop value. Add 1 when you need to include it.
range() is lazy — 48 bytes of memory regardless of how large the range is. Never convert to list just to iterate.
The stop value is a wall the counter hits but never crosses — that mental model will save you from off-by-one errors consistently.
Choosing range() Parameters
IfNeed to repeat something N times starting from 0
UseUse range(n) — gives 0 through n-1
IfNeed numbers 1 through N for display or business logic
UseUse range(1, n+1) — stop is exclusive, so add 1
IfNeed every other element or a custom step size
UseUse range(start, stop, step) — step can be any integer, including negative
IfNeed to loop in reverse from N down to 1
UseUse range(n, 0, -1) — or reversed(my_list) for reversing an existing sequence without index math

Looping Over Strings, Dictionaries, and Using enumerate()

A string in Python is a sequence of characters and you can loop over it exactly the same way you loop over a list. Python hands you one character at a time. This is useful for tasks like counting vowels, reversing text, checking for special characters, or validating a pattern character by character.

Dictionaries work a little differently. When you loop over a dictionary directly, you get its keys — just keys, nothing else. To get both keys and values together in one pass, use .items(). It hands you a (key, value) tuple each round, which you unpack by naming two variables after for. If you only need the values, .values() does that directly. If you only need keys, direct iteration and .keys() are equivalent — .keys() is slightly more explicit and communicates intent to whoever reads the code next.

Here is a pattern change that makes a real difference in code quality: enumerate(). Often you need both the item AND its position number while looping. Without enumerate, a lot of developers create a separate counter variable before the loop and increment it manually on every iteration — three lines where one would do, and a manual increment that's easy to misplace or forget. enumerate(collection) hands you a (index, item) tuple on each pass. You unpack it with two names after for. The start parameter lets you control the starting number, which is handy when displaying positions to users who expect to start counting at 1.

One pattern worth calling out explicitly because it shows up constantly in code reviews: for i in range(len(my_list)): followed by my_list[i] inside the loop. This is the C and Java index loop imported into Python. It works, but it fights the language. It breaks on iterables that don't have a len(), it introduces manual index arithmetic that can be wrong, and it obscures what the code is actually doing. enumerate() is the direct replacement — it works on any iterable, handles the arithmetic for you, and reads as plain English.

advanced_for_loops.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# ── Looping over a string ─────────────────────────────────────────────────────
word = "Python"
print("=== Characters in 'Python' ===")
for character in word:               # each character, one at a time
    print(character)

# ── Looping over a dictionary ─────────────────────────────────────────────────
student_grades = {
    "Alice": 92,
    "Bob": 78,
    "Carlos": 85
}
print("\n=== Keys only (direct iteration) ===")
for student_name in student_grades:          # gives keys by default
    print(student_name)

print("\n=== Keys AND values (.items()) ===")
for student_name, grade in student_grades.items():
    if grade >= 90:
        remark = "Excellent"
    elif grade >= 80:
        remark = "Good"
    else:
        remark = "Keep practising"
    print(f"{student_name}: {grade} — {remark}")

print("\n=== Values only (.values()) ===")
total = sum(student_grades.values())   # .values() passed directly to sum() — no loop needed
print(f"Class total: {total}")

# ── Using enumerate() to track position ───────────────────────────────────────
race_finishers = ["Aisha", "Ben", "Clara", "David"]
print("\n=== Race Results — enumerate(start=1) ===")
for position, runner_name in enumerate(race_finishers, start=1):
    print(f"Position {position}: {runner_name}")

# ── The anti-pattern enumerate() replaces ─────────────────────────────────────
print("\n=== Avoid this pattern: range(len()) ===")
# Works but is unidiomatic — breaks on non-sequence iterables, invites off-by-one errors
for i in range(len(race_finishers)):
    print(f"{i+1}: {race_finishers[i]}")

print("\n=== Prefer enumerate() instead ===")
# Clean, works on any iterable, no manual index arithmetic
for i, runner in enumerate(race_finishers, start=1):
    print(f"{i}: {runner}")
Output
=== Characters in 'Python' ===
P
y
t
h
o
n
=== Keys only (direct iteration) ===
Alice
Bob
Carlos
=== Keys AND values (.items()) ===
Alice: 92 — Excellent
Bob: 78 — Keep practising
Carlos: 85 — Good
=== Values only (.values()) ===
Class total: 255
=== Race Results — enumerate(start=1) ===
Position 1: Aisha
Position 2: Ben
Position 3: Clara
Position 4: David
=== Avoid this pattern: range(len()) ===
1: Aisha
2: Ben
3: Clara
4: David
=== Prefer enumerate() instead ===
1: Aisha
2: Ben
3: Clara
4: David
Pro Tip: Always Prefer enumerate() Over a Manual Counter
If you find yourself writing counter = 0 before a loop and counter += 1 inside it, stop — that's exactly what enumerate() exists to replace. Similarly, for i in range(len(my_list)): followed by my_list[i] is a signal to switch to enumerate(). It's cleaner, less prone to off-by-one errors, and crucially, it works on any iterable — not just lists that support len(). The start parameter is the detail most people miss: enumerate(items, start=1) gives you 1-based positions without any arithmetic.
Production Insight
Looping over a dict directly only gives keys — use .items() for key-value pairs.
Forgetting .items() is a silent bug: the code runs without error but processes only keys, quietly ignoring every value.
The range(len(my_list)) pattern is the most consistent signal that someone learned Python after years of C, Java, or JavaScript — it works, but it's fighting the language rather than using it.
Rule: use enumerate() for indexed iteration over any sequence, dict.items() for key-value iteration. Both add negligible overhead over direct iteration.
Key Takeaway
enumerate() replaces manual counter variables and the range(len()) pattern — always prefer it when you need an index.
Direct dict iteration gives keys only — use .items() when you need values too, .values() when you only need values.
range(len(my_list)) is the C-style index loop — it works but breaks on non-sequence iterables and introduces index arithmetic that enumerate() handles for free.
Choosing the Right Iteration Method
IfNeed only values from a list
UseUse for item in my_list — simplest and most readable form
IfNeed index and value from a list
UseUse for i, item in enumerate(my_list) — never use range(len())
IfNeed only keys from a dictionary
UseUse for key in my_dict — direct iteration gives keys by default
IfNeed keys and values from a dictionary
UseUse for key, value in my_dict.items() — .items() returns (key, value) tuples

Looping Over Two Sequences at Once with zip()

One of the most common loop patterns in real code is processing two related lists in lockstep — pairing names with scores, pairing database keys with fetched values, pairing timestamps with readings. The wrong way is for i in range(len(names)): name = names[i]; score = scores[i]. The right way is zip().

zip() takes two or more iterables and pairs their items together, one from each, yielding a tuple per step. You unpack the tuple by naming two variables after for. When the shorter iterable runs out, zip() stops — no error, no warning, just stops. This is the right behaviour when you know your lists are the same length, but it becomes a silent data loss bug when they differ. If your two sequences might have different lengths and you need all items from both, use itertools.zip_longest() from the standard library, which fills in a configurable default value for the shorter sequence.

Like range(), zip() is lazy in Python 3. It doesn't build a list of tuples upfront — it generates one pair at a time on demand. zip(list_a, list_b) on two million-item lists uses constant memory because it only ever holds one pair in flight.

zip() also makes code self-documenting in a way that index-based loops can't match. When someone reads for name, score in zip(names, scores), the intent is immediately obvious and the pairing is explicit in the code itself. The index-based equivalent requires the reader to mentally trace that names[i] and scores[i] are intentionally paired — an extra cognitive step that zip() eliminates entirely.

One bonus pattern that comes up often: building a dictionary from two parallel lists. dict(zip(keys, values)) is the idiomatic one-liner, and it reads clearly once you know the pattern.

zip_examples.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from itertools import zip_longest

# ── Basic zip(): pair two lists ───────────────────────────────────────────────
names  = ["Alice", "Bob", "Carlos", "Diana"]
scores = [92, 78, 85, 96]

print("=== Score Report ===")
for name, score in zip(names, scores):   # no index math, no range(len())
    status = "Pass" if score >= 80 else "Fail"
    print(f"{name}: {score} — {status}")

# ── zip() stops at the shorter sequence ───────────────────────────────────────
extra_names  = ["Alice", "Bob", "Carlos", "Diana", "Evan"]  # 5 names
fewer_scores = [92, 78, 85]                                   # 3 scores

print("\n=== zip() stops early — Diana and Evan are silently dropped ===")
for name, score in zip(extra_names, fewer_scores):
    print(f"{name}: {score}")
# Diana and Evan never appear — zip() stopped when fewer_scores ran out

# ── zip_longest(): process all items, fill gaps with a default ────────────────
print("\n=== zip_longest() — no items dropped ===")
for name, score in zip_longest(extra_names, fewer_scores, fillvalue="N/A"):
    print(f"{name}: {score}")

# ── zip() with three sequences ────────────────────────────────────────────────
students = ["Alice", "Bob", "Carlos"]
grades   = ["A",     "C",   "B"    ]
years    = [2024,    2025,  2026   ]

print("\n=== Three-way zip ===")
for student, grade, year in zip(students, grades, years):
    print(f"{student} ({year}): {grade}")

# ── zip() to build a dictionary from two lists ────────────────────────────────
headers = ["name", "score", "year"]
row     = ["Alice", 92, 2026]

record = dict(zip(headers, row))      # idiomatic one-liner — cleaner than a manual loop
print(f"\n=== dict from zip: {record}")

# ── zip() is lazy: constant memory regardless of input size ──────────────────
import sys
large_zip = zip(range(1_000_000), range(1_000_000))
print(f"\nzip object memory: {sys.getsizeof(large_zip)} bytes — not 8MB")
Output
=== Score Report ===
Alice: 92 — Pass
Bob: 78 — Fail
Carlos: 85 — Pass
Diana: 96 — Pass
=== zip() stops early — Diana and Evan are silently dropped ===
Alice: 92
Bob: 78
Carlos: 85
=== zip_longest() — no items dropped ===
Alice: 92
Bob: 78
Carlos: 85
Diana: N/A
Evan: N/A
=== Three-way zip ===
Alice (2024): A
Bob (2025): C
Carlos (2026): B
=== dict from zip: {'name': 'Alice', 'score': 92, 'year': 2026}
zip object memory: 72 bytes — not 8MB
Watch Out: zip() Silently Drops Items from the Longer Sequence
If your two lists have different lengths, zip() stops as soon as the shorter one runs out — no error, no warning, no indication that anything was skipped. The extra items from the longer list are silently gone. This is correct behaviour when you know your lists match, but it becomes a quiet data loss bug when they don't. In data pipelines, this is particularly insidious because the output looks valid — it just has fewer rows than it should. If there is any chance the lengths differ, use itertools.zip_longest() and handle the fill value explicitly. Or assert lengths match at the top of the function before zipping: assert len(a) == len(b), f'Expected equal lengths, got {len(a)} and {len(b)}'.
Production Insight
zip() silently drops items from the longer sequence — this is the second most common silent data loss pattern after modifying a list during iteration.
If you are pairing user records with transaction records and one list has 10,001 entries, the 10,001st entry is quietly discarded and your aggregate numbers will be subtly wrong.
Rule: when pairing data from two external sources, assert lengths match before zipping — or use zip_longest with explicit fill handling and validation afterward.
Key Takeaway
zip() pairs items from two or more sequences without index math — it's the replacement for range(len()) when processing parallel lists.
zip() stops at the shorter sequence and silently drops items from the longer one — use zip_longest() when lengths may differ.
zip() is lazy — constant memory regardless of input size, just like range().
Choosing Between zip() and zip_longest()
IfPairing two lists you control and are certain are the same length
UseUse zip() — clean, lazy, and self-documenting. Add an assert if you want to be safe.
IfPairing data from two external sources that might differ in length
UseUse zip_longest() from itertools with an explicit fillvalue — never silently lose data from a longer sequence
IfBuilding a dictionary from two lists (keys and values)
UseUse dict(zip(keys, values)) — the idiomatic one-liner that reads clearly
IfPairing items from three or more sequences simultaneously
Usezip() accepts any number of iterables — for name, score, year in zip(a, b, c) works exactly as you'd expect

Controlling the Loop — break, continue, and the else Clause

Sometimes you don't want to visit every single item. Maybe you're searching a list for a specific value and want to stop the moment you find it — no point checking the remaining 9,000 entries. That's break. It exits the loop immediately, as if you slammed a book shut mid-page.

continue is different — it doesn't stop the loop, it just skips the rest of the current iteration and jumps straight to the next one. Useful when you want to process most items but gracefully skip ones that fail a validation check without adding a layer of nesting.

Python's for loop also has an else clause, and this genuinely surprises most people the first time they encounter it — including some fairly experienced developers. The else block runs only if the loop completed normally, meaning it finished all iterations WITHOUT being interrupted by a break. Think of it as a 'loop completion handler' rather than an alternative condition. It's perfect for search-and-report patterns: loop through items looking for something, break if found, and use else to handle the 'not found' case cleanly without a flag variable.

One important detail that's easy to miss: break and continue only affect the innermost loop they live inside. In a nested loop, break exits the inner loop and returns control to the outer loop — it does not exit both. If you need to exit two levels of nesting at once, the cleanest approach by far is to move the nested logic into a separate function and use return. That eliminates the flag variable pattern, makes the search logic independently testable, and keeps the outer loop readable.

break_continue_else.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# ── break: stop the moment you find what you need ────────────────────────────
passenger_list = ["Tom", "Sara", "Jake", "Mia", "Leo"]
target_passenger = "Jake"

print("=== Boarding check ===")
for passenger in passenger_list:
    if passenger == target_passenger:
        print(f"Found {target_passenger}! Stopping search.")
        break
    print(f"Not {target_passenger}, checked {passenger}")

# ── continue: skip invalid items, keep going ──────────────────────────────────
raw_scores = [88, -5, 76, 0, 95, -1, 60]
print("\n=== Valid scores only ===")
for score in raw_scores:
    if score <= 0:       # invalid — skip this iteration entirely
        continue
    print(f"Recording score: {score}")

# ── for...else: runs ONLY if the loop was NOT broken out of ───────────────────
seating_chart = ["14A", "14B", "15A", "15B"]
requested_seat = "16C"

print("\n=== Seat search ===")
for seat in seating_chart:
    if seat == requested_seat:
        print(f"Seat {requested_seat} found!")
        break
else:
    # This block runs ONLY because we never hit 'break'
    print(f"Seat {requested_seat} is not available.")

# ── break only affects the innermost loop ─────────────────────────────────────
# If you need to exit a nested loop early, use a function + return
print("\n=== Nested loop: break exits inner only ===")
found = False
for row in range(3):
    for col in range(3):
        if row == 1 and col == 1:
            print(f"Found target at row={row}, col={col}")
            found = True
            break          # exits inner loop only — outer loop continues
    if found:
        break              # flag variable lets outer loop exit too — works but verbose

# Cleaner approach: extract nested search into a function
print("\n=== Nested loop: function + return is cleaner ===")
def find_in_grid(grid, target):
    for r, row in enumerate(grid):
        for c, value in enumerate(row):
            if value == target:
                return r, c   # exits both loops immediately, no flag needed
    return None

grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
result = find_in_grid(grid, 5)
print(f"Found 5 at: {result}")
Output
=== Boarding check ===
Not Jake, checked Tom
Not Jake, checked Sara
Found Jake! Stopping search.
=== Valid scores only ===
Recording score: 88
Recording score: 76
Recording score: 95
Recording score: 60
=== Seat search ===
Seat 16C is not available.
=== Nested loop: break exits inner only ===
Found target at row=1, col=1
=== Nested loop: function + return is cleaner ===
Found 5 at: (1, 1)
Interview Gold: The for...else Clause
Most Python developers — including fairly experienced ones — forget that for loops can have an else. Interviewers love asking about this because it signals you've gone beyond surface-level syntax knowledge. The key point: else runs when the loop finishes without a break. Think of it as 'completed without interruption.' A correct answer that also mentions the nested loop exit pattern — where break only exits the innermost loop, and the clean fix is a function with return — will consistently stand out in a technical interview.
Production Insight
break and continue only affect the loop they are directly inside — not any outer loop.
In nested loops, break in the inner loop returns control to the outer loop. Exiting both requires either a flag variable or, better, a function with return.
The flag variable pattern is something most developers have written at some point — it works, but it adds cognitive overhead and couples the exit condition to the outer loop's structure.
Rule: if your nested loop exit logic takes more than two lines to express cleanly, it belongs in a separate function. That function is also independently unit-testable, which the inline flag version isn't.
Key Takeaway
break = stop the entire loop immediately. continue = skip this iteration, keep going.
The for...else clause runs only when the loop completes WITHOUT break — think of it as a 'completed without interruption' handler.
break only exits the innermost loop — use a function with return to cleanly exit nested loops without flag variables.
break vs. continue vs. else Selection
IfFound what you were searching for — stop immediately
UseUse break — exits the loop, skips all remaining iterations
IfCurrent item should be skipped but the loop should continue
UseUse continue — jumps immediately to the next iteration without executing the rest of the body
IfNeed to handle the case where nothing was found after the loop completes
UseUse for...else — the else block runs only if the loop completed without break
IfNeed to exit an outer loop from inside a nested loop
UseMove the nested logic into a function and use return — cleaner than a flag variable and independently testable

List Comprehensions — When a for Loop Fits on One Line

A list comprehension is a compact way to create a new list by transforming or filtering items from an existing collection. It isn't a separate concept from for loops — it IS a for loop, written in a single expression. Python evaluates it by running the loop internally and collecting the results into a new list.

The anatomy: [expression for item in iterable]. With filtering: [expression for item in iterable if condition]. The result is always a new list. The original iterable is untouched.

This matters because the most common for loop pattern in Python is the accumulator: create an empty list, loop, conditionally append. That's three moving parts, each of which can be written slightly wrong. The list comprehension collapses all three into one expression that's harder to get wrong and easier to read at a glance — once you're familiar with the syntax.

There's a readability threshold to keep in mind, though. A comprehension with a simple filter and transform is genuinely cleaner than the accumulator loop. A comprehension with complex nested logic or a multi-step transformation is not — at that point, write the loop explicitly. The goal is readable code, not compact code for its own sake.

Beyond lists, Python has dict comprehensions ({k: v for k, v in items}), set comprehensions ({x for x in items}), and generator expressions ((x for x in items)). Generator expressions deserve special attention: they produce one item at a time rather than building the entire result in memory. If you're passing the result directly to sum(), max(), any(), or all(), use a generator expression instead of a list comprehension — there's no reason to materialise the entire list just to hand it off and immediately discard it.

list_comprehensions.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# ── The accumulator loop pattern (works, but verbose) ────────────────────────
raw_scores = [88, -5, 76, 0, 95, -1, 60, 45, 101]

valid_scores_loop = []
for score in raw_scores:
    if 0 < score <= 100:
        valid_scores_loop.append(score)

print(f"Loop result:   {valid_scores_loop}")

# ── The same logic as a list comprehension ────────────────────────────────────
valid_scores_comp = [score for score in raw_scores if 0 < score <= 100]
print(f"Comprehension: {valid_scores_comp}")

# ── Transformation: apply an operation to every item ─────────────────────────
temps_celsius    = [0, 20, 37, 100]
temps_fahrenheit = [(c * 9/5) + 32 for c in temps_celsius]
print(f"\nFahrenheit: {temps_fahrenheit}")

# ── Filter AND transform in one line ─────────────────────────────────────────
words   = ["hello", "world", "python", "for", "loop"]
# Uppercase only words longer than 4 characters
result  = [w.upper() for w in words if len(w) > 4]
print(f"\nFiltered + uppercased: {result}")

# ── Dict comprehension ────────────────────────────────────────────────────────
students = {"Alice": 92, "Bob": 78, "Carlos": 85}
passing  = {name: grade for name, grade in students.items() if grade >= 80}
print(f"\nPassing students: {passing}")

# ── Generator expression vs list comprehension: memory comparison ─────────────
import sys

list_comp = [x ** 2 for x in range(10_000)]        # builds entire list in memory upfront
gen_expr  = (x ** 2 for x in range(10_000))         # lazy — one value at a time

print(f"\nList comprehension size: {sys.getsizeof(list_comp):,} bytes")
print(f"Generator expression size: {sys.getsizeof(gen_expr)} bytes")

# When summing, use a generator — no need to build the list first
list_sum = sum([x ** 2 for x in range(10_000)])    # builds list, then immediately discards it
gen_sum  = sum(x ** 2 for x in range(10_000))      # constant memory — always prefer this form
print(f"\nBoth produce same sum: {list_sum == gen_sum}")
Output
Loop result: [88, 76, 95, 60, 45]
Comprehension: [88, 76, 95, 60, 45]
Fahrenheit: [32.0, 68.0, 98.6, 212.0]
Filtered + uppercased: ['HELLO', 'WORLD', 'PYTHON']
Passing students: {'Alice': 92, 'Carlos': 85}
List comprehension size: 87,616 bytes
Generator expression size: 112 bytes
Both produce same sum: True
List Comprehension vs Generator Expression: The Memory Rule
  • List comprehension: [expr for item in iterable] — builds the full list immediately, supports indexing and len(), reusable
  • Generator expression: (expr for item in iterable) — lazy, one item at a time, single-use, cannot index into it
  • When passing to sum(), the outer parentheses already exist: sum(x**2 for x in range(n)) — no extra brackets needed
  • If the comprehension body needs multiple lines or complex logic, use a regular for loop — readability wins over compactness
  • Nested comprehensions (list of lists) work but become hard to read quickly — use named for loops when nesting exceeds one level
Production Insight
The most common Python performance issue I flag in code reviews is sum([x for x in large_list if condition]) — building a full list in memory just to sum it and throw it away on the next line.
The fix is one character: remove the square brackets. sum(x for x in large_list if condition) is a generator expression — constant memory, identical result.
Rule: if you are passing a comprehension directly to sum(), any(), all(), max(), or min(), drop the square brackets. The outer function call already provides the parentheses you need.
Key Takeaway
A list comprehension is a for loop in one line — it builds a new list by transforming or filtering items from an existing collection.
Generator expressions use constant memory — prefer them when passing results directly to sum(), any(), all(), max().
Use a regular for loop when the body has side effects — comprehensions are for producing values, not performing actions.
For Loop vs List Comprehension vs Generator Expression
IfBuilding a new list by transforming or filtering an existing one
UseUse a list comprehension — cleaner and typically faster than an accumulator loop
IfPassing the result directly to sum(), max(), any(), all()
UseUse a generator expression (round brackets) — avoids building a list that gets immediately discarded
IfThe loop body has side effects — printing, writing to a file, calling an API, updating a database
UseUse a regular for loop — comprehensions are for producing values, not performing actions with side effects
IfBuilding a dictionary from an iterable
UseUse a dict comprehension: {key: value for key, value in items}

Nested Loops and When to Avoid Them

A nested loop is a loop inside another loop. The outer loop runs once; for each of its iterations, the inner loop runs to completion. If the outer loop has 5 items and the inner loop has 5 items, you get 25 total iterations. If both loops have 1,000 items, you get 1,000,000. This O(n²) growth is the thing to watch — nested loops that perform fine on small development datasets can become genuinely painful on production data at scale.

The canonical correct use for a nested loop is two-dimensional data: a grid, a matrix, a table where you need to visit every cell. Outer loop for rows, inner loop for columns. That's the right tool for that structure.

The anti-pattern is using a nested loop when Python's standard library already has something that does the job more efficiently. The most common case: checking whether any item from list A exists in list B. With nested loops, every item in A is checked against every item in B — O(n²). Converting list B to a set first reduces each membership check to O(1), making the overall algorithm O(n). That difference is irrelevant at 50 items and enormous at 50,000.

For generating every combination of two sequences — every pair, every permutation — itertools.product() is the clean alternative to nested loops. Same output, less code, and the intent ('I want the cartesian product') is explicit in the function name rather than implied by the loop structure.

nested_loops.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from itertools import product

# ── Basic nested loop: multiplication table ───────────────────────────────────
print("=== Multiplication Table (3x3) ===")
for row in range(1, 4):         # outer loop: 3 iterations
    for col in range(1, 4):     # inner loop: 3 iterations per outer iteration = 9 total
        print(f"{row} x {col} = {row * col}")
    print()                     # blank line after each row

# ── Working with a 2D grid ────────────────────────────────────────────────────
print("=== Grid traversal ===")
grid = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]
for row_index, row in enumerate(grid):
    for col_index, value in enumerate(row):
        print(f"  grid[{row_index}][{col_index}] = {value}")

# ── The O(n²) anti-pattern: nested loop membership check ─────────────────────
import time

list_a = list(range(5_000))
list_b = list(range(2_500, 7_500))  # overlaps with list_a in the middle

# BAD: O(n²) — 'in list_b' scans the whole list every single time
start = time.perf_counter()
bad_overlap = [x for x in list_a if x in list_b]
bad_time = time.perf_counter() - start

# GOOD: O(n) — convert list_b to a set once, then each 'in set_b' check is O(1)
set_b = set(list_b)
start = time.perf_counter()
good_overlap = [x for x in list_a if x in set_b]
good_time = time.perf_counter() - start

print(f"\n=== Membership check performance ===")
print(f"Nested loop (O(n²)): {bad_time*1000:.2f}ms, {len(bad_overlap)} overlapping items")
print(f"Set lookup  (O(n)):  {good_time*1000:.2f}ms, {len(good_overlap)} overlapping items")

# ── itertools.product(): cartesian product without nested loops ───────────────
colors  = ["Red", "Blue"]
sizes   = ["S",   "M",   "L"  ]

print("\n=== All colour/size combinations ===")
for color, size in product(colors, sizes):   # cleaner than two nested for loops
    print(f"{color} - {size}")
Output
=== Multiplication Table (3x3) ===
1 x 1 = 1
1 x 2 = 2
1 x 3 = 3
2 x 1 = 2
2 x 2 = 4
2 x 3 = 6
3 x 1 = 3
3 x 2 = 6
3 x 3 = 9
=== Grid traversal ===
grid[0][0] = 1
grid[0][1] = 2
grid[0][2] = 3
grid[1][0] = 4
grid[1][1] = 5
grid[1][2] = 6
grid[2][0] = 7
grid[2][1] = 8
grid[2][2] = 9
=== Membership check performance ===
Nested loop (O(n²)): 187.43ms, 2500 overlapping items
Set lookup (O(n)): 0.61ms, 2500 overlapping items
=== All colour/size combinations ===
Red - S
Red - M
Red - L
Blue - S
Blue - M
Blue - L
When Nested Loops Are the Wrong Tool
  • Nested loops on 100-item lists = 10,000 operations. On 10,000-item lists = 100,000,000 operations.
  • If the inner loop body is just an 'in' check, convert the outer collection to a set before the loop starts
  • itertools.product() generates cartesian products with cleaner syntax and explicit intent — prefer it over nested for loops for combination generation
  • Deeply nested loops — three levels or more — are almost always a signal the code needs to be broken into named functions
  • Profile before optimising — sometimes the O(n²) loop is completely acceptable because n is always small in practice
Production Insight
The set conversion trick — set_b = set(list_b) before the outer loop — is one of the highest-impact single-line performance fixes you can make in Python.
I've watched data pipelines drop from 40 minutes to under 15 seconds by converting one list membership check to a set lookup. The code change was four words.
Rule: if you are checking 'x in some_list' inside a loop and that list has more than a few hundred items, convert it to a set first. The one-time O(n) conversion cost pays for itself after the very first iteration of the outer loop.
Key Takeaway
Nested loops on large data are O(n²) — watch the sizes carefully on anything that touches production data.
Convert a list to a set before membership checks inside a loop — O(n) conversion once, O(1) lookup per iteration from that point on.
Use itertools.product() for cartesian products instead of nested loops — same result, explicit intent.
Three or more levels of nesting is a signal to refactor into named functions, not a style preference.
Nested Loop vs Built-in Alternative
IfProcessing a 2D grid, matrix, or table row by row and column by column
UseNested loops are the right tool — use enumerate() at both levels for clean index tracking
IfChecking which items from list A appear in list B
UseConvert list B to a set, then use a single pass: [x for x in a if x in set_b]
IfGenerating every combination of items from two sequences
UseUse itertools.product(a, b) — avoids nested loop syntax and the intent is explicit in the name
IfLoop is nested three or more levels deep
UseRefactor — extract inner loop logic into named functions. Three levels of nesting is a design problem, not a style preference.
● Production incidentPOST-MORTEMseverity: high

Silent Data Loss from Modifying a List During Iteration

Symptom
Cleanup job reported success but 47% of flagged records still existed in the database. No errors in logs. Script ran in under expected time — which should have been the first warning sign.
Assumption
Python's for loop visits every item in the list sequentially regardless of modifications made during iteration.
Root cause
The script used for item in records: and called records.remove(item) inside the loop body when a condition matched. Removing an item shifts all subsequent indices left by one, but Python's internal iterator keeps advancing its position counter regardless. So the item that slides into the just-vacated slot gets skipped entirely on the next step. Every deletion caused one skip. With roughly half the records matching the delete condition, approximately half ended up being skipped — which explains the 47% figure almost exactly.
Fix
Changed to for item in records[:]: to iterate over a shallow copy while modifying the original list. The better long-term fix: replaced the entire pattern with a list comprehension — records = [r for r in records if not should_delete(r)] — which is both safer and expresses intent more clearly. No mutation, no iterator gymnastics, and the code reads as a declarative statement of what you want to keep rather than a procedural description of what to remove.
Key lesson
  • Never modify a list while iterating over it directly — use a copy or build a new list
  • Silent bugs are worse than crashes — always verify loop results with assertions or counts after the fact
  • List comprehensions are both safer and more Pythonic than in-place modification loops
  • Add logging that reports expected vs. actual item counts after any cleanup operation — 'success' should mean something verifiable
Production debug guideFrom silent skips to infinite hangs7 entries
Symptom · 01
Loop skips every other item during removal
Fix
Check if you are modifying the list in-place — use for item in list[:] or a list comprehension instead
Symptom · 02
range(n) produces unexpected starting number
Fix
Remember range starts at 0 by default — use range(1, n+1) if you need 1 through n
Symptom · 03
for...else block runs when you expect it to be skipped
Fix
Verify the loop actually hits a break — else runs when the loop completes naturally without interruption, not when it breaks
Symptom · 04
NameError on loop variable after loop ends on empty list
Fix
Initialize a result variable before the loop — never rely on the loop variable existing after the loop if the iterable might be empty
Symptom · 05
zip() loop stops before one list is exhausted
Fix
zip() stops at the shortest sequence — use itertools.zip_longest() if you need to process all items from both sequences
Symptom · 06
Loop runs but produces identical results for every iteration
Fix
Check for late-binding closure on loop variables — lambdas and inner functions capture variables by reference, not by value at definition time
Symptom · 07
Performance degrades linearly with input size then suddenly tanks
Fix
Check for nested loops with O(n²) growth — if the inner loop does membership checks on a list, convert that list to a set before the outer loop starts
★ For Loop Quick Debug Cheat SheetFast diagnostics for common for loop failures in production Python services.
Loop silently skips items during in-place list modification
Immediate action
Check for .remove() or del inside a `for item in list:` loop
Commands
python -c "a=[1,2,3,4,5]; [a.remove(x) for x in a if x%2==0]; print(a)"
python -c "a=[1,2,3,4,5]; cleaned=[x for x in a if x%2!=0]; print(cleaned)"
Fix now
Use for item in list[:] or a list comprehension to build a new list — never mutate what you're iterating over
for...else block executes unexpectedly+
Immediate action
Add print statements before break to confirm the break condition is actually hit at runtime
Commands
python -c "for x in [1,2,3]: if x==99: break else: print('else ran — no break occurred')"
grep -n 'break' your_file.py | head -20
Fix now
Remember: else runs only when the loop completes WITHOUT break — it is a loop-completion handler, not an if-else
zip() silently drops items from the longer list+
Immediate action
Print len() of both sequences before zipping to verify they match
Commands
python -c "a=[1,2,3]; b=[1,2,3,4]; print(list(zip(a,b)))"
python -c "from itertools import zip_longest; a=[1,2,3]; b=[1,2,3,4]; print(list(zip_longest(a,b,fillvalue='N/A')))"
Fix now
Use itertools.zip_longest() or assert len(a) == len(b) before zipping when data comes from external sources
Nested loop performance is orders of magnitude slower than expected+
Immediate action
Profile with cProfile to identify the hot loop before touching anything
Commands
python -m cProfile -s cumtime your_script.py
python -c "import time; a=list(range(5000)); b=set(range(2500,7500)); t=time.perf_counter(); [x for x in a if x in b]; print(f'{(time.perf_counter()-t)*1000:.2f}ms')"
Fix now
Convert the inner collection to a set before the loop — O(1) lookup vs O(n) per iteration
Loop variable holds wrong value after loop ends+
Immediate action
Initialize a result variable before the loop instead of relying on the loop variable's post-loop state
Commands
python -c "result = None for x in []: result = x print('result:', result)"
grep -n 'for.*in' your_file.py | grep -A1 'print(.*loop_var'
Fix now
Always initialize result variables before the loop — the loop variable is undefined if the iterable is empty and meaningless if you need a computed result
Loop Type Comparison
Aspectfor Loopwhile Loop
Best use caseKnown collection or fixed number of iterationsRepeat until a condition changes — iteration count unknown upfront
Risk of infinite loopVery low — stops automatically when the collection is exhaustedHigh — easy to forget updating the condition variable
Counting built-inYes, via range() or enumerate() — no manual counter neededNo — you manage your own counter and increment it yourself
ReadabilityVery readable — the iterable makes intent obvious at a glanceCan be harder to scan quickly, especially with complex conditions
Looping over a listNatural — for item in my_listPossible but awkward — requires manual index management
Typical exampleProcess every order in a cart; apply a discount to every itemKeep prompting user until valid input received; poll until connection ready
Memory efficiencyrange() and zip() are lazy — constant memory regardless of iteration countNo built-in lazy generation — you manage memory yourself
Parallel iterationzip() pairs two sequences cleanly and lazilyRequires manual index management for both sequences simultaneously

Key takeaways

1
A for loop visits every item in a collection exactly once
you describe what to do per item, Python handles the counting and advancing automatically.
2
range(stop) starts at 0 and excludes the stop value
range(5) gives 0,1,2,3,4. Use range(1, 6) when you need 1 through 5. range() is lazy — 48 bytes regardless of size, and it supports indexing and membership checks without being converted to a list.
3
Use enumerate() instead of a manual counter variable or range(len())
it works on any iterable, handles index arithmetic automatically, and is the Pythonic standard for indexed iteration.
4
Use zip() to iterate over two sequences in lockstep
the replacement for range(len()) on parallel lists. zip() stops at the shorter sequence silently; use itertools.zip_longest() when lengths may differ and silent data loss would be a problem.
5
The for...else clause runs only if the loop was NOT exited via break
Python's built-in clean pattern for 'search and handle not-found' logic without flag variables.
6
Never modify a list while iterating over it directly
use a shallow copy or a list comprehension. List comprehensions are safer, express intent as a declarative statement, and eliminate mutation entirely.
7
Nested loops are O(n²)
if your inner loop is an 'in list' membership check, convert that list to a set before the outer loop starts. One O(n) conversion pays for itself after the first iteration.
8
Generator expressions use constant memory
when passing results to sum(), any(), all(), max(), drop the square brackets and let Python stay lazy.

Common mistakes to avoid

6 patterns
×

Modifying a list while looping over it directly

Symptom
Loop silently skips every other matching item. Cleanup scripts report success but leave roughly half the target records untouched. No errors, no warnings — just wrong results that surface later downstream.
Fix
Loop over a copy with for item in my_list[:] or build a filtered version with a list comprehension: cleaned = [item for item in my_list if not should_remove(item)]. The list comprehension is the preferred approach — it's safer, reads as a declarative statement of what you want to keep, and doesn't mutate anything.
×

Expecting range(n) to include n in its output

Symptom
Calculations are off by one. A loop meant to process items 1 through 10 only processes 1 through 9. The last item is always silently missing and the output looks almost right, which makes it hard to catch.
Fix
Remember range's stop value is exclusive — it's a wall the counter hits but never crosses. Use range(1, n+1) to get 1 through n inclusive. When in doubt, print list(range(...)) to verify before using it in real logic.
×

Using the loop variable after the loop and trusting its value

Symptom
NameError when the iterable is empty because the loop variable was never created. Or the variable holds the last item processed rather than a meaningful computed result, causing subtle logic errors that only appear with certain input sizes.
Fix
Initialize a result variable before the loop and assign results explicitly inside the body. Never rely on the loop variable existing after the loop ends or assume it holds anything meaningful — especially when the iterable might be empty.
×

Forgetting that for...else runs when the loop does NOT break

Symptom
The else block executes when you expected it to be skipped — or doesn't execute when you expected it to run. The semantics are inverted from what most developers assume coming from other languages.
Fix
Read for...else as: 'if the loop finishes all iterations without hitting break, run this block.' It is not an if-else — it is a loop-completion handler. Rename it mentally to 'for...completed' if that helps the semantics click.
×

Using range(len(my_list)) instead of enumerate() for indexed iteration

Symptom
Code works but is harder to read, breaks on iterables that don't support len(), and introduces manual index arithmetic that creates off-by-one opportunities on every use.
Fix
Use for i, item in enumerate(my_list) — it works on any iterable, handles the index automatically, and reads as natural English. The range(len()) pattern is the C or Java index loop ported into Python where it doesn't belong.
×

Using zip() when the two sequences might have different lengths

Symptom
Items from the longer sequence are silently dropped with no error, no warning, and no indication in the output that anything was missed. The discrepancy only surfaces when someone counts records and notices the numbers don't add up.
Fix
Use itertools.zip_longest(a, b, fillvalue=None) when lengths might differ. Add an assertion before zipping when they must be equal: assert len(a) == len(b), f'Length mismatch: {len(a)} vs {len(b)}' — fail loudly rather than silently losing data.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between break and continue in a Python for loop, ...
Q02SENIOR
Python's for loop has an else clause — what does it do, and when does th...
Q03JUNIOR
If you need both the index and the value when iterating over a list, wha...
Q04SENIOR
What happens if you modify a list while iterating over it with a for loo...
Q05SENIOR
What is zip() and what happens when the two sequences you pass to it hav...
Q06SENIOR
What is the difference between a list comprehension and a generator expr...
Q01 of 06JUNIOR

What is the difference between break and continue in a Python for loop, and can you give a practical example of when you'd use each?

ANSWER
break exits the loop entirely — no more iterations run after it fires. Use it when you've found what you were looking for and there's no point continuing. continue skips only the rest of the current iteration and moves immediately to the next one — the loop keeps going. Use it when you want to process most items but skip ones that fail a condition without adding a nesting level. Example: break when you find a target username in a list; continue to skip negative scores when computing an average of valid entries. One important detail: both break and continue only affect the innermost loop they live inside — in nested loops, break exits the inner loop and returns control to the outer one, not both.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can I use a for loop to loop over a string in Python?
02
What's the difference between a for loop and a while loop in Python?
03
Why does Python use indentation to define the loop body instead of curly braces?
04
What does `for _ in range(n)` mean — what is the underscore?
05
How do I loop over two lists at the same time?
06
What is the difference between a list comprehension and a for loop?
🔥

That's Control Flow. Mark it forged?

10 min read · try the examples if you haven't

Previous
if-elif-else in Python
2 / 7 · Control Flow
Next
while Loop in Python