Python Loops: for, while, enumerate, zip, break, continue, else Clause, Iterators, and Loop Patterns Every Developer Must Know

Python Loops: for, while, enumerate, zip, break, continue, else Clause, Iterators, and Loop Patterns Every Developer Must Know

Loops are how programs do repetitive work — process every row in a dataset, retry a failed API call, scan every file in a directory, or validate every record in a batch. Without loops, you would write the same line of code a million times to process a million rows.

Python has two loops: for (iterate over a sequence) and while (repeat until a condition is false). But the real power comes from the tools AROUND loops — enumerate for index tracking, zip for parallel iteration, break and continue for flow control, and the surprising else clause that most developers do not know exists.

Think of a for loop like a factory assembly line. Each product (item) moves through the station (loop body) one at a time, gets processed, and moves on. The line stops when all products are done. A while loop is like a security camera — it keeps recording (running) as long as motion is detected (condition is True). The moment there is no motion (condition becomes False), it stops.

Table of Contents

  • The for Loop
  • Iterating Over Sequences
  • Iterating Over Strings
  • Iterating Over Dictionaries
  • The range() Function
  • range() Patterns
  • enumerate() — Index + Value
  • Why enumerate Beats range(len())
  • Starting from a Different Number
  • zip() — Parallel Iteration
  • Zipping Multiple Sequences
  • Unequal Lengths and zip_longest
  • Unzipping with zip(*)
  • The while Loop
  • Basic while
  • Infinite Loops and How to Escape
  • while with User Input
  • while for Retry Logic
  • break and continue
  • break — Exit the Loop Early
  • continue — Skip to Next Iteration
  • break and continue in while Loops
  • The else Clause on Loops
  • for-else
  • while-else
  • When else Is Actually Useful
  • Nested Loops
  • Basic Nested Loops
  • Breaking Out of Nested Loops
  • Flattening with Comprehensions
  • Loop Performance Tips
  • Avoid Work Inside Loops
  • Use Comprehensions Over append Loops
  • Use Sets for Membership Checks
  • Avoid Modifying a List While Iterating
  • Common Loop Patterns
  • Accumulator Pattern
  • Search Pattern (Find First Match)
  • Filter Pattern
  • Transformation Pattern
  • Sliding Window
  • Loops in Data Engineering
  • Processing Files in a Directory
  • Paginated API Calls
  • Batch Processing
  • Common Mistakes
  • Interview Questions
  • Wrapping Up

The for Loop

Iterating Over Sequences

# for loops iterate over ANY iterable — lists, tuples, sets, strings, dicts, files, generators
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)
# apple
# banana
# cherry

# The variable "fruit" takes on each value in the list, one at a time
# After the loop, "fruit" still holds the LAST value
print(fruit)  # "cherry" — the last item (not destroyed after the loop)

# Iterating over a tuple
coordinates = (10, 20, 30)
for coord in coordinates:
    print(coord)     # 10, 20, 30

# Iterating over a set (order is NOT guaranteed)
colors = {"red", "green", "blue"}
for color in colors:
    print(color)     # Order may vary between runs

Iterating Over Strings

# Each character is one iteration
for char in "Python":
    print(char, end=" ")     # P y t h o n

# Count vowels in a string
text = "Hello World"
vowel_count = 0
for char in text.lower():
    if char in "aeiou":
        vowel_count += 1
print(f"Vowels: {vowel_count}")  # Vowels: 3

Iterating Over Dictionaries

student = {"name": "Naveen", "age": 30, "city": "Toronto"}

# Default: iterates over KEYS
for key in student:
    print(key)               # name, age, city

# Iterate over values
for value in student.values():
    print(value)             # Naveen, 30, Toronto

# Iterate over key-value pairs (MOST COMMON)
for key, value in student.items():
    print(f"{key}: {value}")
# name: Naveen
# age: 30
# city: Toronto

# Practical: build a summary string
parts = []
for key, value in student.items():
    parts.append(f"{key}={value}")
summary = ", ".join(parts)
print(summary)  # "name=Naveen, age=30, city=Toronto"

The range() Function

range() Patterns

# range(stop) — 0 to stop-1
for i in range(5):
    print(i, end=" ")    # 0 1 2 3 4

# range(start, stop) — start to stop-1
for i in range(2, 7):
    print(i, end=" ")    # 2 3 4 5 6

# range(start, stop, step) — with custom step
for i in range(0, 20, 3):
    print(i, end=" ")    # 0 3 6 9 12 15 18

# Counting backwards
for i in range(10, 0, -1):
    print(i, end=" ")    # 10 9 8 7 6 5 4 3 2 1

# range() is LAZY — does not store values in memory
r = range(1_000_000_000)  # Uses almost zero memory!
print(999_999 in r)        # True — supports "in" checks efficiently

# Common pattern: repeat N times (when you do not need the index)
for _ in range(3):         # _ means "I don't use this variable"
    print("Hello!")        # Prints "Hello!" three times

Real-life analogy: range(5) is like a ticket dispenser at a deli counter — it gives you numbers 0 through 4, one at a time. It does not print all tickets in advance (lazy). It generates the next number only when you ask for it.

enumerate() — Index + Value

Why enumerate Beats range(len())

fruits = ["apple", "banana", "cherry"]

# ❌ UNPYTHONIC — using range(len())
for i in range(len(fruits)):
    print(f"{i}: {fruits[i]}")

# ✅ PYTHONIC — using enumerate
for i, fruit in enumerate(fruits):
    print(f"{i}: {fruit}")
# 0: apple
# 1: banana
# 2: cherry

# enumerate returns tuples of (index, value)
print(list(enumerate(fruits)))  # [(0, 'apple'), (1, 'banana'), (2, 'cherry')]

Real-life analogy: range(len(fruits)) is like looking at a numbered list, then walking to the shelf to find item #0, then #1, then #2. enumerate is like a librarian handing you each book WITH its shelf number — you get both at the same time without a separate lookup.

Starting from a Different Number

# start=1 for human-readable numbering (people count from 1, not 0)
for num, fruit in enumerate(fruits, start=1):
    print(f"{num}. {fruit}")
# 1. apple
# 2. banana
# 3. cherry

# Practical: numbered menu items
menu = ["Home", "About", "Services", "Contact"]
for i, item in enumerate(menu, start=1):
    print(f"  [{i}] {item}")
# [1] Home
# [2] About
# [3] Services
# [4] Contact

# Practical: find index of first match
data = [10, 20, 30, 40, 50]
for i, value in enumerate(data):
    if value > 25:
        print(f"First value > 25 is {value} at index {i}")
        break    # Found it — stop looking
# First value > 25 is 30 at index 2

zip() — Parallel Iteration

Zipping Multiple Sequences

# zip pairs up elements from two or more iterables, step by step
names = ["Alice", "Bob", "Charlie"]
scores = [88, 72, 95]

for name, score in zip(names, scores):
    print(f"{name}: {score}")
# Alice: 88
# Bob: 72
# Charlie: 95

# zip returns tuples
print(list(zip(names, scores)))
# [('Alice', 88), ('Bob', 72), ('Charlie', 95)]

# Three or more sequences
cities = ["Toronto", "London", "Delhi"]
for name, score, city in zip(names, scores, cities):
    print(f"{name} from {city} scored {score}")

# Practical: build a dict from two lists
columns = ["name", "age", "city"]
values = ["Naveen", 30, "Toronto"]
record = dict(zip(columns, values))
print(record)  # {'name': 'Naveen', 'age': 30, 'city': 'Toronto'}

Real-life analogy: zip is like a zipper on a jacket — it interlocks teeth from the left side and right side, one pair at a time. Left tooth #1 pairs with right tooth #1, left #2 with right #2. Two separate strips become one connected unit.

Unequal Lengths and zip_longest

# zip stops at the SHORTEST iterable
short = [1, 2]
long = [10, 20, 30, 40]
print(list(zip(short, long)))   # [(1, 10), (2, 20)] — 30 and 40 are lost!

# zip_longest continues to the LONGEST, filling missing values
from itertools import zip_longest

result = list(zip_longest(short, long, fillvalue=0))
print(result)  # [(1, 10), (2, 20), (0, 30), (0, 40)]

# Practical: compare two datasets column by column
expected = ["customer_id", "name", "email", "city"]
actual = ["customer_id", "name", "email"]

for exp, act in zip_longest(expected, actual, fillvalue="MISSING"):
    status = "✅" if exp == act else "❌"
    print(f"  {status} Expected: {exp}, Got: {act}")
# ✅ Expected: customer_id, Got: customer_id
# ✅ Expected: name, Got: name
# ✅ Expected: email, Got: email
# ❌ Expected: city, Got: MISSING

Unzipping with zip(*)

# zip(*) is the REVERSE of zip — splits paired data back into separate sequences
pairs = [("Alice", 88), ("Bob", 72), ("Charlie", 95)]

names, scores = zip(*pairs)
print(names)    # ('Alice', 'Bob', 'Charlie')
print(scores)   # (88, 72, 95)

# How it works:
# zip(*pairs) = zip(("Alice", 88), ("Bob", 72), ("Charlie", 95))
# = zip across the tuples: (Alice, Bob, Charlie) and (88, 72, 95)

# Practical: transpose a 2D list (rows ↔ columns)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
transposed = list(zip(*matrix))
print(transposed)  # [(1, 4, 7), (2, 5, 8), (3, 6, 9)]

The while Loop

Basic while

# while runs as long as the condition is True
count = 0
while count < 5:
    print(count, end=" ")   # 0 1 2 3 4
    count += 1              # CRITICAL: must change the condition variable!

# Without count += 1, this would run FOREVER (infinite loop)

# while is ideal when you do NOT know how many iterations in advance
# for is better when you DO know (or have a sequence to iterate over)

# Real-world: read until end of file
# while line := file.readline():  # Walrus operator (Python 3.8+)
#     process(line)

Infinite Loops and How to Escape

# Intentional infinite loop — common for servers, event loops, menus
while True:
    command = input("Enter command (quit to exit): ")
    if command.lower() == "quit":
        print("Goodbye!")
        break              # EXIT the loop
    print(f"You entered: {command}")

# In Databricks/Spark: infinite loop for streaming
# while True:
#     micro_batch = read_stream()
#     process(micro_batch)
#     if shutdown_requested:
#         break

Real-life analogy: while True with break is like a restaurant that stays open until the last customer leaves. It does not close at a fixed time — it keeps running and checks after each customer: “anyone still here? No? Close up.” The break is the “close up” signal.

while with User Input

# Validate user input — keep asking until valid
while True:
    age_str = input("Enter your age: ")
    if age_str.isdigit() and 0 < int(age_str) < 150:
        age = int(age_str)
        break
    print("Invalid age. Please enter a number between 1 and 149.")

print(f"Your age is: {age}")

while for Retry Logic

# Retry a flaky API call with exponential backoff
import time

max_retries = 5
attempt = 0
wait_time = 1   # Start with 1 second

while attempt < max_retries:
    try:
        response = call_api()       # Might fail
        if response.status_code == 200:
            print("Success!")
            break                    # Exit on success
        elif response.status_code == 429:   # Rate limited
            print(f"Rate limited. Waiting {wait_time}s...")
        else:
            print(f"Error {response.status_code}")
    except ConnectionError as e:
        print(f"Connection failed: {e}")

    attempt += 1
    time.sleep(wait_time)
    wait_time *= 2  # Exponential backoff: 1, 2, 4, 8, 16 seconds

if attempt == max_retries:
    print("All retries exhausted. Giving up.")

break and continue

break — Exit the Loop Early

# break IMMEDIATELY exits the innermost loop
numbers = [10, 20, 30, 40, 50, 60]

for num in numbers:
    if num > 35:
        print(f"Found {num} — stopping!")
        break
    print(f"Checking: {num}")
# Checking: 10
# Checking: 20
# Checking: 30
# Found 40 — stopping!
# (50 and 60 are NEVER checked)

# Practical: find the first file that exists
import os
search_paths = ["/data/primary/", "/data/backup/", "/data/archive/"]
data_path = None

for path in search_paths:
    if os.path.exists(path):
        data_path = path
        break

if data_path:
    print(f"Using: {data_path}")
else:
    print("No data path found!")

continue — Skip to Next Iteration

# continue SKIPS the rest of the current iteration and moves to the NEXT item
numbers = [1, -2, 3, -4, 5, -6]

for num in numbers:
    if num < 0:
        continue             # Skip negative numbers
    print(f"Processing: {num}")
# Processing: 1
# Processing: 3
# Processing: 5
# (-2, -4, -6 are skipped)

# Practical: skip invalid records
records = [
    {"id": 1, "name": "Alice", "email": "alice@email.com"},
    {"id": 2, "name": None, "email": "bob@email.com"},
    {"id": 3, "name": "Charlie", "email": None},
    {"id": 4, "name": "Diana", "email": "diana@email.com"},
]

clean_records = []
for record in records:
    if not record.get("name") or not record.get("email"):
        print(f"Skipping record {record['id']} — missing data")
        continue
    clean_records.append(record)

print(f"Clean records: {len(clean_records)}")  # 2 (Alice and Diana)

Real-life analogy: break is like walking out of a movie theater mid-film — you leave entirely. continue is like skipping a boring scene — you fast-forward to the next scene but stay in the theater.

break and continue in while Loops

# break in while — exit the loop
while True:
    data = read_next_batch()
    if data is None:
        break    # No more data — exit
    process(data)

# continue in while — skip current iteration
attempts = 0
while attempts < 10:
    attempts += 1   # MUST increment BEFORE continue, or infinite loop!
    result = try_operation()
    if result == "skip":
        continue     # Skip processing, try next attempt
    process(result)
    if result == "done":
        break

The else Clause on Loops

Python has a little-known feature: for-else and while-else. The else block runs ONLY if the loop completed normally (without break). If break was used, else is skipped.

for-else

# else runs if the loop finished WITHOUT break
numbers = [2, 4, 6, 8]

for num in numbers:
    if num % 2 != 0:
        print(f"Found odd number: {num}")
        break
else:
    print("All numbers are even")   # This runs — no break was hit
# Output: "All numbers are even"

# Now with an odd number:
numbers = [2, 4, 5, 8]

for num in numbers:
    if num % 2 != 0:
        print(f"Found odd number: {num}")
        break
else:
    print("All numbers are even")   # SKIPPED — break was hit
# Output: "Found odd number: 5"

while-else

# Same concept with while
attempts = 0
while attempts < 3:
    result = try_connect()
    if result == "success":
        print("Connected!")
        break
    attempts += 1
else:
    # Only runs if the while condition became False (all attempts exhausted)
    print("Failed to connect after 3 attempts")

When else Is Actually Useful

# Searching for an item — else handles "not found"
def find_user(users, target_id):
    for user in users:
        if user["id"] == target_id:
            return user
    else:
        return None   # No break = not found (else technically unnecessary here due to return)

# Validating that ALL items pass a check
def all_valid(records):
    for record in records:
        if not record.get("email"):
            print(f"Invalid record: {record['id']}")
            break
    else:
        print("All records are valid!")   # Only if no break
        return True
    return False

# Think of else as "no break" — it runs when the loop was NOT interrupted
# Many Pythonistas find this confusing and prefer explicit flags instead:
found = False
for item in items:
    if item == target:
        found = True
        break
if not found:
    print("Not found")

Nested Loops

Basic Nested Loops

# Outer loop × inner loop = total iterations
# If outer has 3 items and inner has 4, that is 12 iterations
for i in range(3):
    for j in range(4):
        print(f"({i},{j})", end=" ")
    print()   # Newline after each row
# (0,0) (0,1) (0,2) (0,3)
# (1,0) (1,1) (1,2) (1,3)
# (2,0) (2,1) (2,2) (2,3)

# Practical: compare every pair
teams = ["Alpha", "Beta", "Gamma"]
for i in range(len(teams)):
    for j in range(i + 1, len(teams)):   # Start at i+1 to avoid duplicates
        print(f"{teams[i]} vs {teams[j]}")
# Alpha vs Beta
# Alpha vs Gamma
# Beta vs Gamma

# Multiplication table
for i in range(1, 6):
    for j in range(1, 6):
        print(f"{i*j:4d}", end="")
    print()

Breaking Out of Nested Loops

# break only exits the INNERMOST loop
for i in range(3):
    for j in range(3):
        if j == 1:
            break           # Only breaks the inner loop!
        print(f"({i},{j})", end=" ")
    print("[inner broke]")
# (0,0) [inner broke]
# (1,0) [inner broke]
# (2,0) [inner broke]
# Outer loop still runs all 3 times!

# To break out of BOTH loops, use a flag or a function:

# Method 1: Flag variable
found = False
for i in range(10):
    for j in range(10):
        if i * j == 42:
            print(f"Found: {i} × {j} = 42")
            found = True
            break
    if found:
        break

# Method 2: Extract to a function (cleaner)
def find_product(target):
    for i in range(10):
        for j in range(10):
            if i * j == target:
                return (i, j)   # Exits the entire function
    return None

result = find_product(42)
print(result)  # (6, 7)

Flattening with Comprehensions

# Nested loops for simple transformations → use comprehensions instead
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# Nested loop version:
flat = []
for row in matrix:
    for num in row:
        flat.append(num)

# Comprehension version (same result, one line):
flat = [num for row in matrix for num in row]
print(flat)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Reading order: for row in matrix → for num in row → num
# The comprehension reads LEFT to RIGHT, same order as the nested loops

Loop Performance Tips

Avoid Work Inside Loops

# ❌ BAD — calling len() on every iteration (recalculated each time)
for i in range(len(data)):
    if i < len(data) - 1:    # len(data) calculated EVERY iteration
        process(data[i])

# ✅ GOOD — calculate once, use many
data_len = len(data)
for i in range(data_len):
    if i < data_len - 1:
        process(data[i])

# ❌ BAD — repeated string concatenation in a loop (O(n²))
result = ""
for word in words:
    result += word + ","     # Creates a new string EVERY iteration

# ✅ GOOD — join at the end (O(n))
result = ",".join(words)

# ❌ BAD — function lookup in tight loop
for item in big_list:
    item.upper()             # Looks up .upper method every iteration

# ✅ GOOD — cache the method reference
upper = str.upper
for item in big_list:
    upper(item)              # Direct call, no attribute lookup

Use Comprehensions Over append Loops

# ❌ SLOW — append in a loop
squares = []
for x in range(1000000):
    squares.append(x ** 2)

# ✅ FAST — list comprehension (20-50% faster)
squares = [x ** 2 for x in range(1000000)]

# Why? Comprehensions are optimized at the C level.
# The append loop has Python function call overhead on every iteration.

Use Sets for Membership Checks

# ❌ SLOW — checking "in" against a list inside a loop (O(n) per check)
valid_ids = [101, 102, 103, ..., 999]    # Large list
for order in orders:                      # 100,000 orders
    if order.id in valid_ids:             # O(n) check × 100,000 = very slow
        process(order)

# ✅ FAST — convert to set first (O(1) per check)
valid_ids_set = set(valid_ids)            # One-time O(n) conversion
for order in orders:
    if order.id in valid_ids_set:         # O(1) check × 100,000 = fast
        process(order)

Avoid Modifying a List While Iterating

# ❌ DANGEROUS — modifying while iterating causes skipped items
nums = [1, 2, 3, 4, 5, 6]
for num in nums:
    if num % 2 == 0:
        nums.remove(num)    # Modifies the list during iteration!
print(nums)                  # [1, 3, 5] — looks correct but 4 was SKIPPED!

# ✅ SAFE — iterate over a copy
nums = [1, 2, 3, 4, 5, 6]
for num in nums[:]:          # [:] creates a copy to iterate over
    if num % 2 == 0:
        nums.remove(num)
print(nums)                  # [1, 3, 5] — correct, nothing skipped

# ✅ BETTER — use a comprehension (creates a new filtered list)
nums = [1, 2, 3, 4, 5, 6]
nums = [num for num in nums if num % 2 != 0]
print(nums)                  # [1, 3, 5]

Common Loop Patterns

Accumulator Pattern

# Start with an initial value, build up through iteration
# SUM
total = 0
for price in [10.50, 20.00, 15.75, 8.25]:
    total += price
print(f"Total: ${total:.2f}")  # Total: $54.50

# COUNT with condition
error_count = 0
for log_line in log_lines:
    if "ERROR" in log_line:
        error_count += 1
print(f"Errors: {error_count}")

# BUILD a dict
word_counts = {}
for word in text.split():
    word_counts[word] = word_counts.get(word, 0) + 1

Search Pattern (Find First Match)

# Find the first item matching a condition
def find_first(items, condition):
    for item in items:
        if condition(item):
            return item
    return None

# Usage
users = [{"name": "Alice", "active": False}, {"name": "Bob", "active": True}]
first_active = find_first(users, lambda u: u["active"])
print(first_active)  # {'name': 'Bob', 'active': True}

# Python built-in: next() with a generator
first_active = next((u for u in users if u["active"]), None)
print(first_active)  # Same result, one line

Filter Pattern

# Keep only items matching a condition
# Loop version:
large_orders = []
for order in orders:
    if order["amount"] > 100:
        large_orders.append(order)

# Comprehension version (preferred):
large_orders = [o for o in orders if o["amount"] > 100]

# filter() version:
large_orders = list(filter(lambda o: o["amount"] > 100, orders))

Transformation Pattern

# Transform each item in a collection
# Loop version:
upper_names = []
for name in names:
    upper_names.append(name.upper())

# Comprehension version (preferred):
upper_names = [name.upper() for name in names]

# map() version:
upper_names = list(map(str.upper, names))

Sliding Window

# Process consecutive pairs/triples from a sequence
data = [10, 20, 30, 40, 50]

# Consecutive pairs (sliding window of size 2)
for i in range(len(data) - 1):
    pair = (data[i], data[i + 1])
    print(f"Pair: {pair}, Diff: {data[i+1] - data[i]}")
# Pair: (10, 20), Diff: 10
# Pair: (20, 30), Diff: 10
# Pair: (30, 40), Diff: 10
# Pair: (40, 50), Diff: 10

# Using zip for cleaner sliding window
for current, next_val in zip(data, data[1:]):
    print(f"{current} → {next_val}")

# Practical: detect sudden changes in a time series
prices = [100, 102, 98, 150, 155, 90]
for i, (prev, curr) in enumerate(zip(prices, prices[1:])):
    change_pct = abs(curr - prev) / prev * 100
    if change_pct > 20:
        print(f"Alert at index {i+1}: {change_pct:.1f}% change ({prev} → {curr})")

Loops in Data Engineering

Processing Files in a Directory

import os
from pathlib import Path

# Process all CSV files in a directory
data_dir = Path("/data/incoming/")
csv_files = sorted(data_dir.glob("*.csv"))

print(f"Found {len(csv_files)} CSV files")
for i, file_path in enumerate(csv_files, start=1):
    print(f"[{i}/{len(csv_files)}] Processing: {file_path.name}")
    # df = spark.read.csv(str(file_path), header=True)
    # process(df)

# Skip files that are too small (likely empty)
for file_path in csv_files:
    if file_path.stat().st_size < 100:  # Less than 100 bytes
        print(f"Skipping {file_path.name} — too small")
        continue
    print(f"Processing {file_path.name} ({file_path.stat().st_size:,} bytes)")

Paginated API Calls

import requests

def fetch_all_pages(base_url, headers):
    """Fetch all pages from a paginated API."""
    all_records = []
    page = 1

    while True:   # Keep going until no more pages
        response = requests.get(
            base_url,
            params={"page": page, "per_page": 100},
            headers=headers,
            timeout=30
        )
        response.raise_for_status()
        data = response.json()

        records = data.get("results", [])
        if not records:
            break   # No more data — exit

        all_records.extend(records)
        print(f"Page {page}: {len(records)} records (total: {len(all_records)})")

        if not data.get("has_next", False):
            break   # API says no more pages

        page += 1

    return all_records

Batch Processing

# Process a large list in chunks of N
def chunks(lst, n):
    """Yield successive n-sized chunks from lst."""
    for i in range(0, len(lst), n):
        yield lst[i:i + n]

# Usage: process 1000 records in batches of 100
all_records = list(range(1000))
batch_size = 100

for batch_num, batch in enumerate(chunks(all_records, batch_size), start=1):
    print(f"Batch {batch_num}: processing {len(batch)} records ({batch[0]}-{batch[-1]})")
    # process_batch(batch)
    # time.sleep(0.5)  # Rate limiting between batches

# Output:
# Batch 1: processing 100 records (0-99)
# Batch 2: processing 100 records (100-199)
# ...
# Batch 10: processing 100 records (900-999)

Common Mistakes

  1. Using range(len(list)) instead of enumeratefor i in range(len(fruits)): print(fruits[i]) is verbose and error-prone. Use for i, fruit in enumerate(fruits): for both index and value.
  2. Forgetting to increment in a while loopwhile count < 10: without count += 1 runs forever. Always ensure the condition variable changes inside the loop.
  3. Modifying a list while iterating over it — removing items during iteration skips elements. Iterate over a copy (for item in list[:]:) or use a comprehension to create a filtered list.
  4. Using break expecting it to exit nested loopsbreak only exits the innermost loop. Use a flag variable or extract the nested loops into a function with return.
  5. Building strings with += in a loop — each += creates a new string (O(n) per iteration, O(n²) total). Use join() for O(n) performance.
  6. Using a list for in checks inside a loopif x in my_list is O(n). Convert to a set before the loop for O(1) lookups — especially important when the loop runs thousands of times.
  7. Placing continue after the increment in while loops — if continue is before count += 1, the counter never increments on skipped iterations, causing an infinite loop. Always increment before continue.

Interview Questions

Q: What is the difference between break and continue? A: break exits the loop entirely — no more iterations run. continue skips the rest of the current iteration and moves to the next item. break leaves the theater; continue skips a scene.

Q: What is enumerate() and why is it better than range(len())? A: enumerate() yields (index, value) pairs during iteration. range(len()) gives only the index, requiring list[i] for the value — more verbose and error-prone. enumerate is cleaner, more Pythonic, and works with any iterable (not just lists).

Q: How does the else clause on a for loop work? A: The else block runs only if the loop completed without hitting break. If break was executed, else is skipped. Think of it as “no break” — it runs when the loop finished naturally. Useful for search loops: the loop searches for an item, break on found, else handles “not found.”

Q: How does zip() work and what happens with unequal lengths? A: zip() pairs elements from multiple iterables step by step: first elements together, second elements together, etc. With unequal lengths, it stops at the shortest iterable (extra elements are ignored). Use itertools.zip_longest(fillvalue=default) to continue to the longest with fill values.

Q: Why should you avoid modifying a list while iterating over it? A: Removing items shifts subsequent indices, causing the loop to skip elements. For example, removing index 2 makes the old index 3 become index 2, but the loop advances to index 3 — skipping what was originally at index 3. Iterate over a copy (list[:]) or use a comprehension to create a new filtered list.

Q: When would you use a while loop instead of a for loop? A: Use while when you do not know how many iterations in advance — user input validation (keep asking until valid), API pagination (keep fetching until no more pages), retry logic (keep trying until success or max retries), and event loops (keep running until shutdown signal). Use for when iterating over a known sequence.

Wrapping Up

Loops are how programs handle repetition. for iterates over sequences — use it when you have a collection to process. while repeats until a condition changes — use it for retry logic, user input, and event loops. enumerate gives you index and value together. zip pairs up parallel sequences. break exits early. continue skips one iteration.

The performance tips matter in data engineering: use comprehensions over append loops, sets over lists for membership checks, and join() over += for string building. These are not micro-optimizations — they are the difference between a pipeline that runs in seconds and one that runs for minutes.

Previous in this series: Conditionals — if/elif/else, Ternary, Match-Case

Next in this series: Functions — Parameters, Return Values, Scope, *args, **kwargs


Naveen Vuppula is a Senior Data Engineering Consultant and app developer based in Ontario, Canada. He writes about Python, SQL, AWS, Azure, and everything data engineering at DriveDataScience.com.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top
Privacy Policy · About
Share via
Copy link