WasiLearn Code
Python · Control flow

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}")
output
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)
output
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)
output
  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()
output
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)
output
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))
output
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")
output
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}")
output
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}")
output
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}")
output
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])
output
[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