Nested For Loops

A loop inside a loop, the grid it walks, and why the work multiplies rather than adds.

Overview

The order things happen

for row in range(3):
    for col in range(4):
        print(row, col)

The outer loop takes row = 0 and then hands control to the inner loop, which runs all four of its passes before the outer loop moves to row = 1. Twelve lines print, in row-major order. The inner variable resets each time; the outer one does not.

This is the natural shape for anything two-dimensional: a grid, a table of rows and columns, every pairing of two lists.

nested.py

nested.py Python 3
Output

                    

nested_cost.py

nested_cost.py Python 3
Output

                    

Worth knowing

The inner loop finishes entirely on every single pass of the outer one.
Two nested loops over n items run n×n bodies. Doubling n quadruples the work; it does not double it.
break leaves only the loop it is in, the inner one. The outer loop carries on.
If both loops walk the same list, you are usually comparing every pair. That is often a sign a set or dict would do it in one pass.

Nested For Loops: A Practical Guide

A loop inside a loop runs the inner one from the top on every pass of the outer one. That is easy to say and easy to under-estimate: the bodies multiply, they do not add.

Where the newline goes

A detail that catches people: print() after the inner loop, indented to the outer loop, ends the row. Indent it one level further and you get a newline after every cell instead. The indentation is the logic here, not decoration.

The cost multiplies

One loop over n items runs n bodies. Two nested loops over n items run n×n. Put them side by side instead of inside each other and you get 2n. The difference between n² and 2n is the difference between a program that scales and one that does not:

  • n = 100: nested runs 10,000 bodies, sequential runs 200
  • n = 400: nested runs 160,000, sequential runs 800

Doubling the input quadruples nested work. The second program on this page times both at three sizes so the shape is visible rather than asserted.

Reading a grid, row by row

The most common nested loop walks a two-dimensional structure:

grid = [[1, 2, 3], [4, 5, 6]]
for row in grid:
    for value in row:
        print(value, end=" ")
    print()

The outer loop takes a row - itself a list - and the inner loop takes the values in it. Note that the outer variable holds a whole row, not an index, which is what makes this read better than the for i in range(len(grid)) version.

When you need the coordinates as well, enumerate supplies them at both levels:

for r, row in enumerate(grid):
    for c, value in enumerate(row):
        print(r, c, value)

Breaking out of both

break leaves one loop. Three ways to leave two, in order of preference:

def find(grid, target):              # 1. a function, and return
    for row in grid:
        for value in row:
            if value == target:
                return value
found = None                         # 2. a flag the outer loop checks
for row in grid:
    for value in row:
        if value == target:
            found = value
            break
    if found is not None:
        break

The function is almost always the right answer: return leaves everything, and the search gets a name.

When nesting is hiding a better idea

Two loops over the same collection compare every pair, and that is n squared work for a question that often has a one-pass answer.

for a in nums:                      # every pair - slow
    for b in nums:
        if a + b == target:
            ...
seen = set()                        # one pass
for n in nums:
    if target - n in seen:
        ...
    seen.add(n)

The rule of thumb: if the inner loop is searching for something, a set or a dictionary usually removes it. If the two loops walk genuinely different things - rows and columns, users and permissions - the nesting is correct and there is nothing to remove.

Reading the cost without timing anything

You can tell the cost of a nested loop by reading it. Count how many times each loop runs and multiply. Two loops over the same n-item list run n times n. A loop over n containing a loop over m runs n times m. A loop over n containing a lookup in a set runs n times, because the lookup does not loop.

That last one is the important case. A nested loop and a loop containing a set lookup can look almost identical on the page and behave completely differently at scale, and the difference is invisible on ten items and decisive on ten thousand.

This is also why the timings on this page are run at three sizes rather than one. A single measurement tells you how long something took; three tell you how the time grows, which is the thing that actually matters when your data gets bigger.

Depth, and when to stop

Two levels of nesting is ordinary. Three is worth a second look. Four is nearly always a sign that some of the structure belongs in a function, because by then nobody can hold all the loop variables in their head at once, and the indentation alone pushes the real work off the right of the screen.

Pulling the inner loops into a named function costs nothing at runtime and gives each level a name that says what it is iterating over. It also gives you return, which is the cleanest way out of nested loops there is.

A worked example: a formatted table

Nested loops and the alignment that makes their output readable:

for row in range(1, 4):
    for col in range(1, 4):
        print(f"{row * col:4}", end="")
    print()
   1   2   3
   2   4   6
   3   6   9

Two details do all the work. end="" on the inner print keeps the cells on one line, and the bare print() after the inner loop — indented to the outer loop — ends the row. Indent that second print one level further and every cell gets its own line; remove it and the whole table becomes one long line. The indentation is the logic.

f"{value:4}" right-aligns each cell in four characters, which is what stops the columns drifting once the numbers reach two digits. Counting spaces by hand works until the data changes; a width does not.

itertools.product, the flat version

When the nesting exists only to produce every combination, product flattens it into one loop:

from itertools import product

print([f"{x}{y}" for x, y in product("ab", [1, 2])])
['a1', 'a2', 'b1', 'b2']

The order is the same as the nested loops it replaces: the last argument varies fastest, exactly as the innermost loop does. product takes any number of iterables, and repeat= gives the same one several times — product(range(6), repeat=3) is every three-dice roll without three levels of indentation.

It is lazy, so it costs nothing until iterated, and it is the right tool when the combinations are the point: parameter sweeps, test matrices, every pairing of two lists. It is the wrong tool when the inner loop depends on the outer one — iterating the cells of each row, where the rows differ in length — because product pairs fixed sequences and cannot look at the outer value to decide what the inner one should be.

itertools.combinations and permutations cover the related questions, and both avoid the nested loop plus index arithmetic that would otherwise be needed to compare every pair without comparing anything with itself.

The comprehension form, and its reading order

Nested loops have a comprehension equivalent, and the clause order is the thing people get wrong:

grid = [[1, 2], [3, 4]]

print([value for row in grid for value in row])
[1, 2, 3, 4]

The clauses appear in the same order as the equivalent nested for statements — outer first, inner second. That is worth stating plainly because the guess most people make is the opposite, and the wrong order raises a NameError about the inner variable, which at least fails loudly.

What is genuinely confusing is a *nested comprehension*, where one comprehension appears inside another's expression: [[f(x) for x in row] for row in grid] produces a list of lists rather than a flat one. Here the reading order really does invert — the outer clause is on the right and the inner work on the left.

Between the two, flattening with two for clauses is common and readable, and a comprehension inside a comprehension is where most people should stop and write the loop. The rule from elsewhere in the track applies: if it needs decoding rather than reading, the loop was the better answer.

Let the data decide the loops

A nested loop is usually a description of the data's shape, and when the two disagree the loop is the thing that is wrong.

If the data is a list of rows and each row is a list of cells, two loops are correct and the outer variable should be a row rather than an index. If the data is a flat list and the nesting exists to pair items with each other, the loops are doing a search and can often be replaced. If the data is a dictionary of lists, the outer loop takes items() and the inner takes the list, and the key is available at both levels without any bookkeeping.

The mismatch worth watching for is a nested loop over data that is already flat. Code that iterates a list of records and then, inside, iterates the same list to find a matching record is describing a join, and a dictionary keyed on the join field turns it into one pass. The nesting was never about the shape of the data; it was a linear search wearing a loop.

The opposite mismatch is a single loop over data that is genuinely nested, usually with manual index arithmetic to work out where each row starts. That is a flattened structure being reconstructed by hand, and reshaping the data once is easier than getting the arithmetic right at every use.

The general habit: write the loops that match the structure you have, and if they are awkward, change the structure rather than the arithmetic.

Measuring, rather than guessing

The cost of a nested loop can be read off the page, and when it matters it is still worth measuring, because the constant factors are not visible in the notation.

Two things make a measurement useful. Run it at several input sizes, not one: a single number tells you how long something took, and three tell you how the time grows, which is the property that decides whether the code survives larger data. Doubling the input should roughly double a linear loop and quadruple a nested one, and seeing that ratio confirms which you have.

And measure the thing itself, not the setup. timeit exists because a naive timing includes interpreter warm-up, the cost of building the test data, and whatever else happens to be in the block. It runs the snippet many times and reports the best, which is the number least polluted by everything else on the machine.

The result is often surprising in the useful direction: a nested loop over fifty items is instant and not worth changing, while a linear loop that does something expensive per item can be the real cost. Reading the structure tells you how it scales; measuring tells you whether it matters yet.

Questions people ask

Can I use the same variable name in both loops? You can, and the inner one shadows the outer for the rest of the body. It is legal and confusing.

Does continue in the inner loop skip the outer one? No, it moves to the next inner iteration only.

How do I break out of both loops? Put them in a function and return, which is cleaner than any flag.

Is a nested loop always slow? No — it is slow when both loops grow with the input. A loop over n containing a loop over a fixed three items is linear.

What if the inner loop needs the outer index? enumerate at both levels gives you both, without any range(len(...)).

Why does my inner variable still have a value after the loop? Because a for loop does not have its own scope. The last value survives, unlike in a comprehension.

Is product faster than nested loops? Slightly, and that is not the reason to use it. It is flatter to read.

Should I worry about nesting depth for performance? Worry about how many times each loop runs, not how deep they are. Three shallow loops over three items each is nine iterations.

Can I nest a comprehension inside a loop? Yes, and it often reads well — the loop handles the structure and the comprehension handles one row.

Is there a limit on nesting depth? Python allows about twenty levels of indentation, which is far past readability. Treat three as the practical ceiling.

How do I loop over two grids together? zip the outer sequences, then loop the pair: for row_a, row_b in zip(a, b).

Recap in one screen

  • The inner loop runs completely on every pass of the outer one; the bodies multiply rather than add.
  • Where the trailing print() is indented decides where rows end — the indentation is the logic, not the formatting.
  • break leaves one loop; a function and return is the clean way out of both.
  • Two loops over the same collection is n squared, and an inner loop that is searching can usually be replaced by a set or a dictionary.
  • itertools.product flattens loops that exist only to produce combinations.

Check yourself

0 of 3

Answer without scrolling back up.

  1. Two loops nested over the same 500-item list. How many inner bodies run?

  2. A `break` in the inner loop of a nested pair does what?

  3. You double the size of the input to a doubly-nested loop. The work:

Cheat sheet

Nested For Loops

A loop inside a loop runs the inner one from the top on every pass of the outer one. That is easy to say and easy to under-estimate: the bodies multiply, they do not add.

PYTHON · vizlearn.in/python/nested_for_loops.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.