Loops inside loops
A loop repeats a block. Put a loop inside that block and the inner one runs completely for every single pass of the outer one. That is the whole idea, and also the whole danger.
sizes = ["small", "large"]
drinks = ["chai", "coffee"]
for size in sizes:
for drink in drinks:
print(f"{size} {drink}")
small chai small coffee large chai large coffee
Two sizes and two drinks give four lines. The inner loop started over from the beginning each time the outer one moved on.
The passes multiply
This is the number to keep in your head: outer times inner.
count = 0
for i in range(3):
for j in range(4):
count = count + 1
print(count)
12
Three and four give twelve. Ten and ten give a hundred. A thousand and a thousand give a million, and a program that appears to have hung.
That multiplication is why nested loops are fine over small, known collections and worth rethinking over large ones. If both lists come from a database, the nesting is usually a sign you wanted a lookup rather than a search.
Grids and tables
The natural use is anything two-dimensional.
for row in range(1, 4):
line = ""
for col in range(1, 4):
line = line + f"{row * col:3}"
print(line)
1 2 3 2 4 6 3 6 9
The inner loop builds one row as a string; the outer loop prints it and starts the next. Printing inside the inner loop instead would put every number on its own line, which is the commonest thing to get wrong here.
{row * col:3} is an f-string width specifier — pad this value to three
characters — which is what keeps the columns lined up.
Nested lists work the same way:
grid = [
[1, 2, 3],
[4, 5, 6],
]
for row in grid:
for value in row:
print(value, end=" ")
print()
1 2 3 4 5 6
end=" " tells print to finish with a space instead of a newline. The bare
print() after the inner loop supplies the line break.
break only leaves the loop it is in
Here is the misunderstanding worth fixing, because the code looks right.
found = None
for row in [[1, 2], [3, 4], [5, 6]]:
for value in row:
if value == 3:
found = value
break
print(f"finished row {row}")
print(found)
finished row [1, 2] finished row [3, 4] finished row [5, 6] 3
The break fired on the second row, and the outer loop carried on through the
third anyway. break leaves one level only. There is no break 2 in
Python.
The clean way out of both is a flag, or better, a function:
def find_first(grid, target):
for row in grid:
for value in row:
if value == target:
return value
return None
print(find_first([[1, 2], [3, 4], [5, 6]], 3))
print(find_first([[1, 2], [3, 4]], 99))
3 None
return leaves the function entirely, however deep you are. That is the
idiomatic answer to "break out of everything", and it is a good reason to pull
a nested search into its own function.
for...else, the one nobody expects
Python loops can have an else, and it runs when the loop finished without
breaking.
seats = [1, 2, 3]
for seat in seats:
if seat == 9:
print("found seat 9")
break
else:
print("seat 9 is not in the list")
seat 9 is not in the list
Read else here as "no break". It is genuinely useful for search loops and it
is also easy to misread, so a comment is worth it when you use it.
Avoiding the nesting
Often the nesting is not needed at all.
Two lists in step — use zip, not an index loop:
names = ["Ayesha", "Bilal", "Sana"]
marks = [72, 45, 88]
for name, mark in zip(names, marks):
print(f"{name}: {mark}")
Ayesha: 72 Bilal: 45 Sana: 88
zip stops at the shorter list, silently. That is usually what you want and
occasionally hides a bug, so check the lengths if they should match.
Every combination — itertools.product flattens the nesting into one
loop:
from itertools import product
for size, drink in product(["small", "large"], ["chai", "coffee"]):
print(f"{size} {drink}")
small chai small coffee large chai large coffee
Same output as the first example, one level of indentation, and break now
leaves the whole thing.
You need the position as well — enumerate:
for index, name in enumerate(["Ayesha", "Bilal"], start=1):
print(f"{index}. {name}")
1. Ayesha 2. Bilal
Flattening
Turning a list of lists into one list is a nested loop that a comprehension says in a line:
grid = [[1, 2], [3, 4], [5, 6]]
flat = []
for row in grid:
for value in row:
flat.append(value)
print(flat)
print([value for row in grid for value in row])
[1, 2, 3, 4, 5, 6] [1, 2, 3, 4, 5, 6]
The comprehension's two for clauses read in the same order as the loops they
replace: outer first, inner second. That order is the one thing people get
backwards, and the comprehensions lesson goes into
it properly.
When nesting is right
Two levels over small collections, building a grid or comparing every pair — that is what nesting is for, and it is perfectly readable. Three levels is a signal: give the inner two a name and call it, and the outer loop becomes a sentence rather than a puzzle.
Next: lists, which is what most of these loops are walking over.
Prefer learning with a teacher?
WasiLearn Academy runs small-group classes covering this material, online and in Karachi.
See the classes