LambdaLabTM
Computer Science · Class 11 · Iterative Statements
IterationA loop in a loop⏱️ 12 min read

Nested for Loops

Nothing new to learn here — no keyword, no syntax. A nested loop is a loop written inside the body of another loop, and the whole lesson is one fact: the inner loop runs all the way through, from the beginning, on every single round of the outer one.

1Why put a loop inside a loop?

One loop prints the table of 7. What prints the tables of 2, 3 and 4? You need to repeat the whole table — and the table is itself a repetition. A repetition of a repetition is a nested loop.

Anything with rows and columns is the same shape: a seating plan, a calendar month, a pattern of stars, marks for every student in every subject. The outer loop takes the rows; the inner loop takes what is in one row.

2How it is written

shape.py
for i in range(1, 4):          <- outer header
    for j in range(1, 5):      <- inner header: indented, so it IS the outer body
        print(i, j)            <- inner body: indented twice
    print('row done')          <- outer body again, after the inner loop ends
print('all done')              <- at the margin: outside both loops

The indentation is doing all the work, exactly as it has since the if statement — only now there are two levels of it. Four spaces means “inside the outer loop”. Eight means “inside the inner loop”. Back at the margin means outside both.

Key Takeaway
The inner loop is the outer loop's body. That is the whole trick. The outer loop does not know it contains a loop — it just runs its block once per round, and its block happens to be a loop that runs four times. So the inner body runs 3 × 4 = 12 times, not 3 + 4 = 7.

And each loop needs its own loop variable. The outer one is traditionally i and the inner one j. Using i for both would mean the inner loop overwriting the outer loop's value on every round — which is not an error, and does not do what anybody wants.

3Watch both counters

Step through it and watch j go all the way from 1 to 4 before i moves at all — and then start again from 1.

🧬 A loop inside a loop

3 outer rounds, 4 inner rounds each. Count the cells as they fill.

nested.py
outer headerinner headerbody — the indented blockoutside
for i in range(1, 4):
for j in range(1, 5):
print(i, j)
outer i
inner j
rounds of the inner body0 / 12
i = 1
1 11 21 31 4
i = 2
2 12 22 32 4
i = 3
3 13 23 33 4

Three rows, four cells each. 3 × 4 = 12, not 3 + 4 — the inner loop starts again from the beginning on every row.

Nothing has run yet. There are two loops here: the outer one counts i through 1, 2, 3 and the inner one counts j through 1, 2, 3, 4.

4Three tables from one program

The outer loop picks the table; the inner loop prints the rows of it. The print() calls between them are worth studying — where a statement sits decides how often it happens:

tables_grid.py
# the tables of 2, 3 and 4 — a loop inside a loop
for n in range(2, 5):
    print('Table of', n)

    for i in range(1, 4):
        print(n, 'x', i, '=', n * i)

    print()
Output
Table of 2
2 x 1 = 2
2 x 2 = 4
2 x 3 = 6

Table of 3
3 x 1 = 3
3 x 2 = 6
3 x 3 = 9

Table of 4
4 x 1 = 4
4 x 2 = 8
4 x 3 = 12

print('Table of', n) is in the outer body, so it runs three times — once per table. The multiplication line is in the inner body, so it runs nine times. And the empty print() at the end of the outer body puts a blank line after each table, which is why the output has three blocks rather than one wall of numbers.

Watch Out
Move a line one indentation level and the program changes completely. Indent print('Table of', n) by four more spaces and it joins the inner loop — printing the heading before every single row, nine times. Nothing about that is an error, so nothing will tell you.

5Counting the rounds

Exam questions ask this constantly, and the counting is simple once you stop adding:

counter.py
# how many times does the inner body run?
count = 0

for i in range(1, 4):
    for j in range(1, 5):
        count = count + 1

print('The outer body ran 3 times')
print('The inner body ran', count, 'times')
Output
The outer body ran 3 times
The inner body ran 12 times

Three values in the outer range, four in the inner: 3 × 4 = 12. Multiply, do not add — because the inner loop starts over from its first value on each round of the outer, rather than carrying on from where it stopped.

6When the inner loop depends on the outer one

The inner range does not have to be a fixed set of numbers. It can be built out of the outer loop variable — and then each row is a different length. That is how patterns are made:

stars.py
# a triangle of stars, built one row at a time
for row in range(1, 5):
    line = ''

    for star in range(1, row + 1):
        line = line + '*'

    print(line)
Output
*
**
***
****

On the first round row is 1, so the inner range is range(1, 2) — one star. On the fourth it is range(1, 5) — four. The inner loop is not counting to a fixed number; it is counting to whatever the outer round is.

Note also where line lives. It is created inside the outer body but outside the inner one: emptied at the start of each row, added to by every star in that row, and printed once the row is complete. Above the outer loop it would collect every star in the triangle into one line; inside the inner loop it would be emptied after each star and only ever print one.

Note
Why build a string instead of printing each star? print() moves to a new line every time, so printing stars one at a time would give a vertical column. There is a way to tell print() not to — a later lesson — so for now the row is built up with + and printed once, which works everywhere and makes the row visible as a value.

7Try it

Run it, then change range(1, 5) to range(1, 8). Then try moving the print(line) in by four spaces and see the triangle grow a step at a time.

stars.py

8Recap

Key Takeaway
A nested loop is a loop inside another loop's body. The inner one runs completely, from its first value, on every round of the outer one — so the inner body runs outer × inner times. Each loop needs its own loop variable, and the indentation level of every statement decides which loop it belongs to.
Quick Check

for i in range(1, 4): / for j in range(1, 5): / print(i, j) — how many lines are printed?

Quick Check

In the tables program, what happens if print('Table of', n) is indented to sit inside the inner loop?

Quick Check

In the star triangle, why is line = '' inside the outer loop rather than above it?