Ever faced a problem where you need to generate all possible combinations of something, but the number of "levels" isn't known until runtime? Maybe you're building a product configurator, generating test cases, or exploring a search space. You need nested loops, but how many? That's determined by a variable n
.
You can't write for i in range(x): for j in range(x): for k in range(x):
because you don't know how many loops you'll need. This is where programmatic loop generation comes in - a classic computer science challenge with some elegant solutions.
Let's explore the best approaches, from the most intuitive to the most Pythonic.
Imagine you want to generate all combinations of n
numbers, where each number can be from 0 to some maximum value. For example:
n=2
and max=2
: [0,0], [0,1], [1,0], [1,1]
n=3
and max=2
: [0,0,0], [0,0,1], [0,1,0]
, ... [1,1,1]
The total combinations will be max^n
, which can grow quickly!
Recursion is perhaps the most intuitive way to think about nested loops. Each recursive call represents one level of nesting.
The concept:
n
- we have a complete combinationdef generate_nested_loops_recursive(
n: int,
max_val: int,
current_level: int = 0,
current_combination: list = None
):
"""
Generates all combinations simulating n nested loops using recursion.
Args:
n: Number of nested loops (depth)
max_val: Upper limit for each loop (exclusive)
current_level: Current recursion depth (internal use)
current_combination: Current path being built (internal use)
"""
if current_combination is None:
current_combination = []
# Base case: we have a complete combination
if current_level == n:
print(f"Combination: {current_combination}")
# In real use, you might yield this or pass to another function
return
# Recursive case: iterate for the current level
for i in range(max_val):
current_combination.append(i) # Make a choice
# Recurse to the next level
generate_nested_loops_recursive(
n, max_val, current_level + 1, current_combination
)
current_combination.pop() # Backtrack for next iteration
# Example usage
print("Generating 3 nested loops, each from 0 to 1:")
generate_nested_loops_recursive(3, 2)
Output:
Generating 3 nested loops, each from 0 to 1:
Combination: [0, 0, 0]
Combination: [0, 0, 1]
Combination: [0, 1, 0]
Combination: [0, 1, 1]
Combination: [1, 0, 0]
Combination: [1, 0, 1]
Combination: [1, 1, 0]
Combination: [1, 1, 1]
Pros:
Cons:
Python's itertools
module is built for exactly this kind of problem. The product()
function computes the Cartesian product of input iterables - which is exactly what nested loops do!
import itertools
def generate_nested_loops_itertools(n: int, max_val: int):
"""
Generates all combinations using itertools.product.
Args:
n: Number of nested loops (depth)
max_val: Upper limit for each loop (exclusive)
"""
single_loop_range = range(max_val)
# product with repeat=n simulates n nested loops
for combination in itertools.product(single_loop_range, repeat=n):
print(f"Combination: {list(combination)}")
# Example usage
print("\nGenerating 3 nested loops using itertools.product:")
generate_nested_loops_itertools(3, 2)
Output:
Generating 3 nested loops using itertools.product:
Combination: [0, 0, 0]
Combination: [0, 0, 1]
Combination: [0, 1, 0]
Combination: [0, 1, 1]
Combination: [1, 0, 0]
Combination: [1, 0, 1]
Combination: [1, 1, 0]
Combination: [1, 1, 1]
Pros:
Cons:
This is my recommended approach for most use cases. It's clean, fast, and idiomatic Python.
You can also simulate nested loops iteratively by maintaining a list of indices, similar to how an odometer works.
def generate_nested_loops_iterative(n: int, max_val: int):
"""
Generates all combinations using iterative index manipulation.
Args:
n: Number of nested loops (depth)
max_val: Upper limit for each loop (exclusive)
"""
if n == 0:
return
# Initialize all indices to 0
indices = [0] * n
while True:
# Print current combination
print(f"Combination: {indices.copy()}")
# Increment the rightmost index (like an odometer)
position = n - 1
while position >= 0:
indices[position] += 1
if indices[position] < max_val:
# No overflow, we're done incrementing
break
# Overflow: reset this position and carry to the left
indices[position] = 0
position -= 1
# If we've carried past the leftmost position, we're done
if position < 0:
break
# Example usage
print("\nGenerating 3 nested loops using iterative approach:")
generate_nested_loops_iterative(3, 2)
Output:
Generating 3 nested loops using iterative approach:
Combination: [0, 0, 0]
Combination: [0, 0, 1]
Combination: [0, 1, 0]
Combination: [0, 1, 1]
Combination: [1, 0, 0]
Combination: [1, 0, 1]
Combination: [1, 1, 0]
Combination: [1, 1, 1]
Pros:
Cons:
Let's say you want to generate all possible n-character passwords using a limited character set:
import itertools
def generate_passwords(length: int, charset: str):
"""Generate all possible passwords of given length from charset."""
count = 0
for password_tuple in itertools.product(charset, repeat=length):
password = ''.join(password_tuple)
count += 1
# In real use, you'd try this password
if count <= 10: # Just show first 10
print(password)
print(f"\nTotal passwords: {count}")
# Generate all 3-character passwords using digits
print("3-digit numeric passwords:")
generate_passwords(3, '0123456789')
Output:
3-digit numeric passwords:
000
001
002
003
004
005
006
007
008
009
Total passwords: 1000
Let's benchmark the different approaches:
import time
import itertools
def benchmark(n, max_val):
# Recursion
start = time.time()
count = 0
def recurse(level, combo):
nonlocal count
if level == n:
count += 1
return
for i in range(max_val):
recurse(level + 1, combo + [i])
recurse(0, [])
recursive_time = time.time() - start
# itertools
start = time.time()
count = sum(1 for _ in itertools.product(range(max_val), repeat=n))
itertools_time = time.time() - start
print(f"\nn={n}, max_val={max_val} ({max_val**n} combinations)")
print(f"Recursion: {recursive_time:.4f}s")
print(f"itertools: {itertools_time:.4f}s")
print(f"Speedup: {recursive_time/itertools_time:.2f}x")
# Run benchmarks
benchmark(3, 10)
benchmark(4, 8)
benchmark(5, 6)
Typical results show itertools.product
is 2-3x faster than recursion, thanks to its C implementation.
Use itertools.product
when:
Use recursion when:
Use iterative when:
What if each "loop" needs a different range? itertools.product
handles this beautifully:
import itertools
# Different ranges for each position
colors = ['red', 'blue']
sizes = ['S', 'M', 'L']
materials = ['cotton', 'polyester']
# Generate all product variants
for variant in itertools.product(colors, sizes, materials):
print(f"{variant[0]} {variant[1]} {variant[2]}")
Output:
red S cotton
red S polyester
red M cotton
red M polyester
...
blue L polyester
This is incredibly useful for e-commerce product variants, test case generation, or configuration exploration!
You might be tempted to use exec()
to build loop code as a string. Don't do it. It's:
# DON'T DO THIS!
code = "for i in range(2):\n"
code += " for j in range(2):\n"
code += " print([i, j])"
exec(code) # Evil!
Generating n nested loops programmatically is a classic problem with elegant solutions:
itertools.product
- it's fast, clean, and PythonicThe pattern you choose depends on your language, constraints, and use case. But in Python, itertools.product
wins for most real-world scenarios.
Now go forth and generate those combinations! Just remember: with great power comes great computational complexity. A 10-level nested loop with 10 iterations each is 10 billion combinations. Choose your parameters wisely! 😄