Python Nested For Loop: Syntax and Performance
Practical guide to python nested for loops: syntax, use cases, performance implications, and how to avoid common mistakes.
A python nested for loop places one for statement inside another. The inner loop runs to completion for every iteration of the outer loop. This structure is common when working with multidimensional data, generating combinations, or comparing elements across two collections. Understanding how the loops interact is essential for writing correct and efficient code.
Basic Structure of a Nested For Loop
The simplest form of a nested loop is a for statement inside another for statement. The inner loop repeats its entire body for each value produced by the outer loop.
for i in range(3): for j in range(2): print(i, j)
This prints six lines: 0 0, 0 1, 1 0, 1 1, 2 0, 2 1. The outer loop variable i stays fixed while the inner loop iterates through all values of j. Once the inner loop finishes, the outer loop advances to the next i and the inner loop starts over.
The same pattern works with any iterable, not just range. Lists, tuples, strings, and generator expressions can all be used in either loop.
When a Nested Loop Is the Right Tool
Nested loops are a natural fit when the problem involves two independent sequences that must be combined or compared. Common scenarios include:
- Traversing a 2D matrix or grid, where the outer loop selects the row and the inner loop selects the column.
- Generating all pairs from two lists, such as checking every product against every supplier.
- Building a Cartesian product of small collections when the full result is needed.
For example, to print the coordinates of a 3x3 grid:
for row in range(3): for col in range(3): print(f"({row}, {col})")
This works because each cell is uniquely identified by its row and column indices. A single loop cannot produce that pairing directly without extra arithmetic or data structures.
Reading Order and Variable Scope
Execution order matters. The outer loop begins first, and the inner loop runs completely before the outer loop moves to its next iteration. This means the inner loop body sees the current value of the outer variable, but the outer loop does not see changes made to the inner variable after the inner loop ends.
Variable scope in Python is function-level, not block-level. Variables defined inside a loop are still accessible after the loop finishes. That can be useful, but it also means reusing the same variable name in nested loops can cause subtle bugs. For example:
items = [1, 2, 3] for item in items: for item in items: print(item)
Here the inner loop overwrites the outer item variable. After the inner loop finishes, the outer loop's item now holds the last value from the inner loop, which can break the outer iteration. Using distinct names like outer_item and inner_item avoids this confusion.
Common Pitfalls in Nested Loops
One frequent mistake is modifying a list while iterating over it inside a nested loop. Removing elements during iteration shifts indices and can cause elements to be skipped or processed twice. If removal is necessary, collect the indices first or build a new list.
Another issue is using the same loop variable name in both levels, as shown above. Most linters will flag this, but it is easy to miss in a long function.
A third pitfall is accidentally creating an infinite loop when the inner loop depends on a mutable condition. For instance, if the inner loop condition is based on a list that is being appended to, the loop may never terminate. Always verify that the inner loop has a clear exit condition independent of the outer loop's side effects.
Performance Considerations
The runtime cost of a nested loop is the product of the number of iterations of each loop. If the outer loop runs n times and the inner loop runs m times, the total number of operations is n * m. This is true regardless of the actual work done inside the inner loop. For large n and m, the time can grow quickly.
Reducing the number of inner iterations is often more effective than micro-optimizing the loop body. For example, when comparing elements in a single list, you can avoid duplicate pairs by starting the inner loop from the outer index plus one:
for i in range(len(items)): for j in range(i + 1, len(items)): compare(items[i], items[j])
This halves the number of comparisons compared to a full nested loop. The exact improvement depends on the data, but the principle is general: eliminate redundant work before optimizing the loop mechanics.
Memory usage is usually not a concern for the loop itself, but building large lists inside the inner loop can consume significant memory. If you only need to process each pair and discard it, use a generator expression or process values directly instead of accumulating them.
Alternatives to Nested Loops
Python's standard library provides tools that can replace nested loops in specific situations. itertools.product generates the Cartesian product of iterables without explicit nesting:
from itertools import product for a, b in product(range(3), range(2)): print(a, b)
This is clearer when the nesting is purely combinatorial and you do not need the outer loop variable to control the inner loop's range. It also avoids variable shadowing because each loop variable is distinct.
List comprehensions can also flatten a nested loop into a single expression. For example, building a list of coordinates:
coords = [(x, y) for x in range(3) for y in range(2)]
The comprehension evaluates the loops in the same order as a nested for statement: the first for is the outer loop, and the second is the inner loop. This is concise, but it can become hard to read when the logic is complex. Use it only when the expression is simple and the intent is clear.
Breaking Out of Nested Loops
A break statement inside the inner loop only exits that loop, not the outer one. If you need to stop both loops when a condition is met, you must either use a flag variable or raise an exception. A flag is straightforward:
found = False for i in range(10): for j in range(10): if i * j == 42: found = True break if found: break
After the inner break, the code checks found and breaks the outer loop. This works but adds an extra conditional. Another option is to return from a function, which exits all loops at once. For deeply nested loops, moving the logic into a helper function is often cleaner than managing multiple flags.
Realistic Example: Matrix Transposition
A nested loop is the direct way to transpose a matrix represented as a list of lists. The outer loop iterates over columns, and the inner loop iterates over rows:
def transpose(matrix): rows = len(matrix) cols = len(matrix[0]) result = [[0] * rows for _ in range(cols)] for i in range(rows): for j in range(cols): result[j][i] = matrix[i][j] return result
This works because each element matrix[i][j] moves to result[j][i]. The nested loop visits every cell exactly once. The time complexity is O(rows * cols), which is optimal for this operation because every element must be read and written. A single loop would require manual index arithmetic and would not be clearer.
Edge Cases and Compatibility
Nested loops behave the same across Python 3 versions, but there are a few edge cases worth noting. If the outer iterable is empty, the inner loop never runs. If the inner iterable is empty, the outer loop still runs but does nothing for that iteration. This is usually harmless, but it can mask logic errors if you expect a certain number of inner iterations.
When the inner loop depends on the outer loop's variable, the inner iterable must be recreated each time. For example, for j in range(i) creates a new range object with a different length for each i. This is expected behavior, but be aware that the inner loop's total work is the sum of i values, not a fixed product.
Python's for loop does not support a C-style index variable that persists across iterations. If you need to track the index, use enumerate in the outer loop and pass the index to the inner loop. This is idiomatic and avoids manual counter management.
Finally, nested loops can be combined with else clauses, but the behavior can be surprising. The else block after a for loop runs only if the loop completes without a break. In a nested loop, the else applies to the loop it is attached to, not to the outer loop. Test this carefully if you rely on it, because it is easy to misinterpret which loop the else belongs to.