Loops #
Loops are the mechanism for executing a block of code repeatedly. Python has two main loop constructs — for and while — but the way Python uses them differs significantly from languages like C, Java, or JavaScript. In Python, a for loop isn’t an index-based loop; it’s a loop based on iterating directly over elements. That paradigm, combined with built-ins like enumerate, zip, and comprehensions, makes Python loop code far more expressive and concise. This article covers the idiomatic ways to write loops in Python — including the anti-patterns often brought over from other-language habits.
for Loops
#
A for loop in Python iterates directly over the elements of an iterable — a list, tuple, string, dict, set, generator, or any object implementing the iteration protocol.
Under the hood, Python’s for loop uses the Iterator Protocol. The interpreter first calls iter() on the iterable to get an iterator object, then repeatedly calls next() to fetch the next element until a StopIteration exception is raised.
Look at the Iterator Protocol flow diagram below:
flowchart TD
Start["Start for loop"] --> GetIter["Call iter(iterable) to get an Iterator"]
GetIter --> LoopStart["Call next(iterator) to fetch an element"]
LoopStart --> CheckStop{"Is StopIteration raised?"}
CheckStop -->|No| RunBody["Run the Loop Body with That Element"]
RunBody --> LoopStart
CheckStop -->|Yes| End["Loop Ends Normally"]Understanding this protocol, we can see that Python’s for loop is really a safe, elegant wrapper around an internal exception-handling mechanism.
# Direct iteration — the Python way
fruits = ["apple", "orange", "mango"]
for item in fruits:
print(item)
# → apple
# → orange
# → mango
# Iterating a string — character by character
for letter in "Python":
print(letter, end=" ")
# → P y t h o n
# Iterating tuples
for x, y in [(1, 2), (3, 4), (5, 6)]: # direct unpacking
print(f"x={x}, y={y}")
# Iterating sets (order not guaranteed)
for color in {"red", "green", "blue"}:
print(color)
Anti-Pattern: Iterating by Index #
The most common mistake from developers new to Python is using an index to iterate — a style common in C/Java but unidiomatic in Python:
fruits = ["apple", "orange", "mango"]
# ANTI-PATTERN: index-based iteration — unnecessary in Python
for i in range(len(fruits)):
print(fruits[i])
# CORRECT: iterate directly over elements
for item in fruits:
print(item)
range() — Generating Number Sequences
#
range() produces a sequence of numbers lazily (no list created in memory). It comes in three forms:
# range(stop) — from 0, up to stop-1
for i in range(5):
print(i, end=" ")
# → 0 1 2 3 4
# range(start, stop) — from start, up to stop-1
for i in range(2, 8):
print(i, end=" ")
# → 2 3 4 5 6 7
# range(start, stop, step) — with a specific step
for i in range(0, 20, 5):
print(i, end=" ")
# → 0 5 10 15
# Backward range
for i in range(10, 0, -1):
print(i, end=" ")
# → 10 9 8 7 6 5 4 3 2 1
# ANTI-PATTERN: range() to iterate over an existing collection
numbers = [10, 20, 30, 40, 50]
for i in range(len(numbers)):
print(numbers[i])
# CORRECT: iterate directly
for n in numbers:
print(n)
# When range() is genuinely the right tool:
# 1. Repeating N times without needing the element
for _ in range(3):
send_ping()
# 2. Generating numbers in a certain range
squares = [i**2 for i in range(1, 11)] # [1, 4, 9, 16, ..., 100]
enumerate() — Index and Value at Once
#
When you actually need both the index and the value, use enumerate() — not range(len()):
fruits = ["apple", "orange", "mango"]
# ANTI-PATTERN: range(len()) just to get an index
for i in range(len(fruits)):
print(f"{i}: {fruits[i]}")
# CORRECT: enumerate() gives both at once
for i, item in enumerate(fruits):
print(f"{i}: {item}")
# → 0: apple
# → 1: orange
# → 2: mango
# Start the numbering from another value
for number, item in enumerate(fruits, start=1):
print(f"{number}. {item}")
# → 1. apple
# → 2. orange
# → 3. mango
zip() — Iterating Multiple Collections at Once
#
zip() combines two or more iterables into pairs (tuples), useful for parallel iteration:
names = ["Budi", "Ani", "Citra"]
scores = [85, 92, 78]
# ANTI-PATTERN: index-based iteration over two lists
for i in range(len(names)):
print(f"{names[i]}: {scores[i]}")
# CORRECT: zip() is cleaner and more expressive
for n, v in zip(names, scores):
print(f"{n}: {v}")
# → Budi: 85
# → Ani: 92
# → Citra: 78
# zip with three or more collections
cities = ["Jakarta", "Surabaya", "Bandung"]
for n, v, k in zip(names, scores, cities):
print(f"{n} from {k}: {v}")
# zip behavior: stops at the shortest iterable
a = [1, 2, 3, 4, 5]
b = ["x", "y", "z"]
print(list(zip(a, b))) # → [(1, 'x'), (2, 'y'), (3, 'z')] — elements 4 and 5 dropped
# Use itertools.zip_longest() if you want to keep all elements
from itertools import zip_longest
print(list(zip_longest(a, b, fillvalue="-")))
# → [(1, 'x'), (2, 'y'), (3, 'z'), (4, '-'), (5, '-')]
while Loops
#
A while loop executes a block of code as long as its condition is True. Used when the number of iterations isn’t known up front.
The while loop has dynamic flow control because we can change the course of iteration using break and continue statements, and add an else block that only runs if the loop ends normally.
Look at the full control-flow visualization of a while loop below:
flowchart TD
Start["Start while Loop"] --> EvalCond{"Evaluate Condition?"}
EvalCond -->|False| ElseBlock{"Is there an else block?"}
ElseBlock -->|Yes| RunElse["Execute else Block"]
ElseBlock -->|No| End["Loop Finished"]
RunElse --> End
EvalCond -->|True| RunBody["Run the Code Lines in the while Block"]
RunBody --> CheckContinue{"Found a continue?"}
CheckContinue -->|Yes| EvalCond
CheckContinue -->|No| CheckBreak{"Found a break?"}
CheckBreak -->|Yes| End
CheckBreak -->|No| EvalCondFrom the diagram above, it’s clear that if the loop is forcibly terminated with break, the else block is skipped entirely because the loop didn’t end normally (the evaluation condition never became False).
# Basic pattern
count = 0
while count < 5:
print(count)
count += 1
# Input loop — keep asking until the input is valid
while True:
answer = input("Continue? (y/n): ").lower()
if answer in ("y", "n"):
break
print("Enter 'y' or 'n'")
# Retry with a maximum limit
MAX_ATTEMPTS = 3
attempt = 0
while attempt < MAX_ATTEMPTS:
success = try_connect()
if success:
break
attempt += 1
print(f"Failed, attempt {attempt}/{MAX_ATTEMPTS}")
Always make sure there’s a condition that will stop awhileloop. A loop with no way out (infinite loop) will freeze your program. Thewhile Truepattern is safe as long as there’s a reachablebreakinside the loop.
break, continue, and pass
#
break — Stop the Loop Entirely
#
# break stops the loop from within
numbers = [3, 7, 2, 9, 4, 1, 8]
target = 9
for n in numbers:
if n == target:
print(f"Found: {n}")
break # stop the loop as soon as the target is found
else:
print("Not found") # runs only if the loop finishes WITHOUT break
continue — Skip the Current Iteration
#
# continue skips the rest of the block and moves to the next iteration
for i in range(10):
if i % 2 == 0:
continue # skip even numbers
print(i, end=" ")
# → 1 3 5 7 9
# Practical example: skipping blank lines while reading a file
line_list = ["Budi", "", "Ani", "", "Citra"]
for line in line_list:
if not line.strip():
continue # skip blank lines
process(line)
pass — Empty Placeholder
#
# pass is a statement that does nothing
# useful when a code block is required but not yet implemented
for item in data:
pass # TODO: implement later
# Or for temporarily empty classes/functions
class DataProcessor:
pass # will be filled in later
def compute_bonus():
pass # placeholder
else on Loops — A Little-Known Feature
#
Python has an else clause for for and while that runs when the loop finishes normally (without break):
# for...else
def find_primes(number_list):
for n in number_list:
for divisor in range(2, int(n**0.5) + 1):
if n % divisor == 0:
break # not prime, stop the inner loop
else:
# inner loop finished without break = no divisor = prime
print(f"{n} is a prime number")
find_primes([2, 3, 4, 5, 6, 7, 8, 9, 10])
# → 2 is a prime number
# → 3 is a prime number
# → 5 is a prime number
# → 7 is a prime number
# while...else — practical for search with a retry limit
import time
MAX_RETRY = 5
attempt = 0
while attempt < MAX_RETRY:
if try_db_connection():
print("Connection successful")
break
attempt += 1
time.sleep(2)
else:
# while finished without break = all attempts failed
raise ConnectionError(f"Failed to connect after {MAX_RETRY} attempts")
List Comprehensions #
A list comprehension is a concise, Pythonic way to build a new list from iteration, often replacing a for loop that only builds a list:
# ANTI-PATTERN: a loop to build a list
squares = []
for i in range(1, 11):
squares.append(i ** 2)
# CORRECT: list comprehension — more concise and usually faster
squares = [i ** 2 for i in range(1, 11)]
print(squares) # → [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# With a filter condition
even = [i for i in range(20) if i % 2 == 0]
print(even) # → [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# String transformation
name_list = [" budi ", "ANI", "citra"]
clean = [name.strip().title() for name in name_list]
print(clean) # → ['Budi', 'Ani', 'Citra']
# Nested comprehension — matrices
matrix = [[i * j for j in range(1, 4)] for i in range(1, 4)]
print(matrix)
# → [[1, 2, 3], [2, 4, 6], [3, 6, 9]]
# Flattening nested lists
nested = [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
flat = [item for sublist in nested for item in sublist]
print(flat) # → [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Dict comprehension
prices = {"apple": 5000, "orange": 8000, "mango": 12000}
# Apply a 10% discount to all prices
discounted = {name: int(price * 0.9) for name, price in prices.items()}
print(discounted)
# → {'apple': 4500, 'orange': 7200, 'mango': 10800}
# Set comprehension
numbers = [1, 2, 2, 3, 3, 3, 4]
unique_squares = {n ** 2 for n in numbers}
print(unique_squares) # → {1, 4, 9, 16}
Generator Expressions #
A generator expression is like a list comprehension but uses plain parentheses () and produces values one at a time lazily — it never builds the whole collection in memory:
# List comprehension: builds the entire list in memory at once
square_list = [i**2 for i in range(1_000_000)] # ~8MB of memory
# Generator expression: computes one at a time as needed
square_gen = (i**2 for i in range(1_000_000)) # almost zero memory
# Useful for operations that only need a single pass
total = sum(i**2 for i in range(1_000_000)) # efficient, no list needed
has_negative = any(n < 0 for n in data) # stops at the first negative element
all_positive = all(n > 0 for n in data) # stops at the first non-positive element
Important Built-in Iteration Functions #
Python provides many built-in functions that work with iteration:
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
# Aggregation
print(sum(numbers)) # → 39
print(min(numbers)) # → 1
print(max(numbers)) # → 9
print(len(numbers)) # → 10
# sorted() — returns a new sorted list (doesn't change the original)
print(sorted(numbers)) # → [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]
print(sorted(numbers, reverse=True)) # → [9, 6, 5, 5, 4, 3, 3, 2, 1, 1]
# Sort by a custom criterion
students = [("Budi", 85), ("Ani", 92), ("Citra", 78)]
by_score = sorted(students, key=lambda s: s[1], reverse=True)
print(by_score) # → [('Ani', 92), ('Budi', 85), ('Citra', 78)]
# filter() — keep elements that satisfy a condition
positive = list(filter(lambda n: n > 0, [-3, 1, -2, 4, -1, 5]))
print(positive) # → [1, 4, 5]
# map() — transform every element
squares = list(map(lambda n: n**2, [1, 2, 3, 4, 5]))
print(squares) # → [1, 4, 9, 16, 25]
# any() and all()
print(any(n > 8 for n in numbers)) # → True (something is > 8)
print(all(n > 0 for n in numbers)) # → True (everything is > 0)
itertools — Advanced Iteration Combinations
#
The itertools module provides powerful, memory-efficient iteration tools:
from itertools import chain, islice, product, combinations, permutations
# chain — merge several iterables into one
a = [1, 2, 3]
b = [4, 5, 6]
c = [7, 8, 9]
for n in chain(a, b, c):
print(n, end=" ")
# → 1 2 3 4 5 6 7 8 9
# islice — take the first N elements from an iterable (lazy)
for n in islice(range(1_000_000), 5):
print(n, end=" ")
# → 0 1 2 3 4
# product — cartesian product (like nested for loops)
for color, size in product(["red", "blue"], ["S", "M", "L"]):
print(f"{color}-{size}", end=" ")
# → red-S red-M red-L blue-S blue-M blue-L
# combinations — combinations without repetition
for combo in combinations([1, 2, 3, 4], 2):
print(combo, end=" ")
# → (1,2) (1,3) (1,4) (2,3) (2,4) (3,4)
# permutations — every distinct arrangement
for perm in permutations("ABC", 2):
print("".join(perm), end=" ")
# → AB AC BA BC CA CB
Summary #
- Iterate directly, not by index —
for item in collectionrather thanfor i in range(len(collection)). It’s cleaner, safer, and faster.enumerate()to get the index and value at once — replacesrange(len())in every case.zip()for parallel iteration over two or more collections — stops at the shortest iterable; usezip_longestif you need every element.for...elseruns when the loop finishes withoutbreak— a clean idiom for the “search and report if not found” pattern.- List comprehensions replace loops that only build a list — more concise and usually faster. Dict and set comprehensions are also available.
- Generator expressions for large data — just like comprehensions but lazy, using almost no extra memory.
any()andall()with generator expressions short-circuit early — efficient for condition checks on large collections.itertoolsfor advanced iteration patterns —chain,islice,product,combinations,permutationseliminate the need for complex nested loops.