Python Nested Loops: When to Use Them and When to Refactor Them Away
A nested loop is a loop inside another loop. For every iteration of the outer loop, the inner loop runs to completion. If the outer loop runs m times and the inner loop runs n times, the body of the inner loop executes m × n times. That multiplication is both the power and the danger of nested loops.
Basic Mechanics
for row in range(3): for col in range(4): print(f"({row},{col})", end=" ") print() # newline after each row
# (0,0) (0,1) (0,2) (0,3)# (1,0) (1,1) (1,2) (1,3)# (2,0) (2,1) (2,2) (2,3)The inner loop completes all four iterations for each single iteration of the outer loop. The total number of print() calls is 3 × 4 = 12.
Common Use Cases
2D Data (Matrices)
matrix = [ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12],]
# Sum all elementstotal = 0for row in matrix: for value in row: total += value
print(total) # 78
# Transpose: swap rows and columnsrows = len(matrix)cols = len(matrix[0])transposed = [[matrix[r][c] for r in range(rows)] for c in range(cols)]print(transposed)# [[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]Generating All Combinations
sizes = ["S", "M", "L", "XL"]colours = ["Red", "Blue", "Green"]
combinations = []for size in sizes: for colour in colours: combinations.append(f"{colour} {size}")
print(len(combinations)) # 12print(combinations[:4])# ['Red S', 'Blue S', 'Green S', 'Red M']This is equivalent to a Cartesian product — every size paired with every colour.
Comparing All Pairs
scores = [88, 92, 75, 88, 60, 92]
# Find all pairs of equal scoresduplicates = []for i in range(len(scores)): for j in range(i + 1, len(scores)): # j starts after i to avoid duplicates if scores[i] == scores[j]: duplicates.append((i, j, scores[i]))
print(duplicates)# [(0, 3, 88), (1, 5, 92)]Searching in a 2D Grid
grid = [ [0, 0, 1, 0], [0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1],]
targets = []for row in range(len(grid)): for col in range(len(grid[row])): if grid[row][col] == 1: targets.append((row, col))
print(targets) # [(0, 2), (2, 1), (3, 3)]Breaking Out of Nested Loops
Python does not have a “break outer loop” keyword. The standard workarounds are:
Option 1: Use a flag variable
found = Falsefor i in range(5): for j in range(5): if i * j == 12: found = True result = (i, j) break if found: break
print(result) # (3, 4)Option 2: Extract into a function and return
def find_product(target, max_val): for i in range(max_val + 1): for j in range(max_val + 1): if i * j == target: return (i, j) return None
print(find_product(12, 5)) # (2, 6) — first matchThis is usually cleaner. Returning from a function immediately exits all loops.
Option 3: Use exceptions (in specific circumstances)
class Found(Exception): pass
try: for i in range(100): for j in range(100): if i ** 2 + j ** 2 == 625: raise Found(i, j)except Found as e: print(f"Found: {e.args}") # Found: (7, 24)This is unusual but occasionally the cleanest option when the target is buried deep in multiple nesting levels.
The O(n²) Problem
Nested loops over inputs of the same size produce quadratic time complexity. If each list has n items, the nested loop runs n² iterations.
import time
def find_common_naive(list1, list2): """O(n²) — check every pair""" common = [] for item in list1: for other in list2: if item == other: common.append(item) return common
def find_common_fast(list1, list2): """O(n) — use a set for O(1) lookup""" set2 = set(list2) return [item for item in list1 if item in set2]
# With 10,000 items, naive takes ~100,000,000 comparisons# fast takes ~20,000 operationsWhen you need to check membership or find intersections, convert one collection to a set rather than nesting loops.
Comprehensions as Alternatives
List comprehensions can replace simple nested loops more compactly:
# Nested loop versionpairs = []for x in range(3): for y in range(3): if x != y: pairs.append((x, y))
# Comprehension versionpairs = [(x, y) for x in range(3) for y in range(3) if x != y]
print(pairs)# [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]Comprehensions run the same iterations — they are syntactic sugar, not a performance shortcut. But they are often more readable for simple cases.
When Not to Nest
Refactor if: the nested loop could be replaced by a set, a dict lookup, or itertools.product.
Refactor if: the total loop count is large and growing — what works on 100 items may be unacceptably slow on 10,000.
Refactor if: understanding the loop requires reading through three or more levels of indentation.
import itertools
# itertools.product is equivalent to nested for loopsfor size, colour in itertools.product(sizes, colours): print(f"{colour} {size}")itertools.product is both more readable (its name tells you what it does) and just as fast as the equivalent nested loops.
Best Practices
- Limit nesting to two levels in most cases. Three is occasionally necessary. Four or more is a design smell.
- Name loop variables descriptively:
for row in matrixrather thanfor i in matrix. - If the inner loop body is more than a few lines, extract it into a function.
- Profile before optimising — not every nested loop is a performance problem.