Python - Nested Loops
A nested loop is simply a loop inside another loop. The inner loop executes completely for each single iteration of the outer loop. This structure is fundamental when you need to work with...
Key Insights
- Nested loops multiply iterations—an outer loop of 100 items with an inner loop of 100 items means 10,000 total iterations, making performance awareness critical
- The
breakandcontinuestatements only affect the innermost loop; escaping multiple levels requires flags, exceptions, or refactoring into functions - When you find yourself nesting beyond two or three levels, it’s usually a sign to refactor—extract inner loops into well-named functions or use built-in tools like
itertools
Introduction to Nested Loops
A nested loop is simply a loop inside another loop. The inner loop executes completely for each single iteration of the outer loop. This structure is fundamental when you need to work with multi-dimensional data, generate combinations, or perform operations that require examining every pair of elements in a collection.
If you’ve ever worked with spreadsheets, you’ve intuitively understood nested loops: you might process each row (outer loop), and for each row, process each column (inner loop). The same logic applies in code.
Basic Syntax and Structure
Python’s indentation-based syntax makes nested loops visually clear. Each level of nesting requires an additional indent. Here’s the basic structure for nested for loops:
for outer_item in outer_sequence:
# Outer loop body
for inner_item in inner_sequence:
# Inner loop body
pass
The same pattern applies to while loops:
outer_counter = 0
while outer_counter < 3:
inner_counter = 0
while inner_counter < 3:
print(f"outer: {outer_counter}, inner: {inner_counter}")
inner_counter += 1
outer_counter += 1
You can also mix loop types—a for loop containing a while loop or vice versa—depending on what makes sense for your use case.
Here’s a practical example that generates a multiplication table:
def print_multiplication_table(size):
for row in range(1, size + 1):
for col in range(1, size + 1):
product = row * col
print(f"{product:4}", end="")
print() # New line after each row
print_multiplication_table(5)
Output:
1 2 3 4 5
2 4 6 8 10
3 6 9 12 15
4 8 12 16 20
5 10 15 20 25
How Nested Loops Execute
Understanding execution order is crucial for debugging and writing correct nested loops. The outer loop starts, then the inner loop runs to completion. Only after the inner loop finishes does the outer loop advance to its next iteration.
Let’s trace through execution explicitly:
for i in range(3):
print(f"Outer loop: i = {i}")
for j in range(4):
print(f" Inner loop: j = {j}")
print(f"Inner loop complete for i = {i}")
print()
Output:
Outer loop: i = 0
Inner loop: j = 0
Inner loop: j = 1
Inner loop: j = 2
Inner loop: j = 3
Inner loop complete for i = 0
Outer loop: i = 1
Inner loop: j = 0
Inner loop: j = 1
Inner loop: j = 2
Inner loop: j = 3
Inner loop complete for i = 1
Outer loop: i = 2
Inner loop: j = 0
Inner loop: j = 1
Inner loop: j = 2
Inner loop: j = 3
Inner loop complete for i = 2
Notice that j resets to 0 each time the outer loop advances. The inner loop is essentially “fresh” for each outer iteration. This is why nested loops with n outer iterations and m inner iterations result in n × m total inner loop executions.
Common Use Cases
Traversing 2D Data Structures
The most common use case is iterating over matrices or grids. When you have a list of lists, nested loops let you access every element:
def find_in_matrix(matrix, target):
"""Find target value in 2D matrix, return (row, col) or None."""
for row_index, row in enumerate(matrix):
for col_index, value in enumerate(row):
if value == target:
return (row_index, col_index)
return None
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
position = find_in_matrix(matrix, 5)
print(f"Found at position: {position}") # Output: Found at position: (1, 1)
Generating Combinations
When you need to examine every pair of elements, nested loops are the natural choice:
def find_pairs_with_sum(numbers, target_sum):
"""Find all pairs of numbers that add up to target_sum."""
pairs = []
for i in range(len(numbers)):
for j in range(i + 1, len(numbers)): # Start at i+1 to avoid duplicates
if numbers[i] + numbers[j] == target_sum:
pairs.append((numbers[i], numbers[j]))
return pairs
numbers = [1, 2, 3, 4, 5, 6]
print(find_pairs_with_sum(numbers, 7)) # Output: [(1, 6), (2, 5), (3, 4)]
Pattern Printing
Nested loops excel at generating text-based patterns:
def print_triangle(height):
"""Print a right triangle of asterisks."""
for row in range(1, height + 1):
for col in range(row):
print("*", end="")
print()
def print_pyramid(height):
"""Print a centered pyramid of asterisks."""
for row in range(1, height + 1):
# Print leading spaces
for space in range(height - row):
print(" ", end="")
# Print asterisks
for star in range(2 * row - 1):
print("*", end="")
print()
print("Triangle:")
print_triangle(5)
print("\nPyramid:")
print_pyramid(5)
Output:
Triangle:
*
**
***
****
*****
Pyramid:
*
***
*****
*******
*********
Controlling Loop Flow with break and continue
Here’s where many developers get tripped up: break and continue only affect the loop they’re directly inside. They don’t magically escape multiple levels of nesting.
# This only breaks the inner loop
for i in range(3):
for j in range(3):
if j == 1:
break # Only exits the inner loop
print(f"i={i}, j={j}")
print(f"Finished inner loop for i={i}")
To break out of multiple loops, you have several options:
Option 1: Flag Variable
def search_with_flag(matrix, target):
found = False
for row_index, row in enumerate(matrix):
for col_index, value in enumerate(row):
if value == target:
print(f"Found {target} at ({row_index}, {col_index})")
found = True
break
if found:
break
Option 2: Extract to Function (Preferred)
def search_matrix(matrix, target):
"""Return position of target or None. Using return exits all loops."""
for row_index, row in enumerate(matrix):
for col_index, value in enumerate(row):
if value == target:
return (row_index, col_index)
return None
# Usage
matrix = [[1, 2], [3, 4], [5, 6]]
result = search_matrix(matrix, 4)
if result:
print(f"Found at {result}")
The function approach is cleaner because return naturally exits all loops. It also makes your code more modular and testable.
Option 3: Exception (Use Sparingly)
class Found(Exception):
pass
try:
for i in range(10):
for j in range(10):
for k in range(10):
if some_condition(i, j, k):
raise Found()
except Found:
print("Exited all loops")
This works but feels like an abuse of exceptions. Reserve it for deeply nested loops where refactoring isn’t practical.
Performance Considerations
Nested loops have multiplicative complexity. A single loop over n items is O(n). Two nested loops over n items each is O(n²). Three levels gives you O(n³). This escalates quickly.
# O(n²) - 1000 items means 1,000,000 iterations
for i in range(1000):
for j in range(1000):
process(i, j)
Before writing nested loops, ask yourself if there’s a better approach.
Refactoring to List Comprehensions
List comprehensions can replace simple nested loops with more readable, often faster code:
# Nested loop approach
result = []
for i in range(5):
for j in range(5):
if i != j:
result.append((i, j))
# List comprehension equivalent
result = [(i, j) for i in range(5) for j in range(5) if i != j]
Using itertools
The itertools module provides optimized functions for common iteration patterns:
from itertools import product, combinations
# Instead of nested loops for all pairs
for i in range(3):
for j in range(3):
print(i, j)
# Use itertools.product
for i, j in product(range(3), range(3)):
print(i, j)
# For combinations without repetition
items = ['a', 'b', 'c', 'd']
for pair in combinations(items, 2):
print(pair) # ('a', 'b'), ('a', 'c'), etc.
Best Practices
Keep nesting shallow. Two levels of nesting is common and acceptable. Three levels should make you pause and consider refactoring. Four or more levels is almost always a code smell.
Use meaningful variable names. Avoid i, j, k when better names exist. Use row, col for matrices. Use student, course when iterating over students and their courses. Clear names make the code self-documenting.
# Hard to follow
for i in items:
for j in i.sub_items:
for k in j.details:
process(k)
# Much clearer
for order in orders:
for line_item in order.line_items:
for discount in line_item.applicable_discounts:
apply_discount(discount)
Extract inner loops to functions. When the inner loop has complex logic, pull it into a well-named function:
def process_order_items(order):
"""Handle all items in a single order."""
for item in order.items:
validate_item(item)
calculate_tax(item)
update_inventory(item)
# Main loop becomes simple
for order in pending_orders:
process_order_items(order)
Consider generators for memory efficiency. When processing large datasets, generators prevent loading everything into memory:
def matrix_elements(matrix):
"""Yield each element with its position."""
for i, row in enumerate(matrix):
for j, value in enumerate(row):
yield i, j, value
# Process elements one at a time
for row, col, value in matrix_elements(large_matrix):
if meets_criteria(value):
handle_element(row, col, value)
Nested loops are a fundamental tool, but like any tool, they require judgment about when and how to use them. Master the basics, understand the performance implications, and know when to reach for alternatives.