Combinations with loop are a classic way to generate all possible selections of items from a sequence without regard to order — useful for picking subsets, creating pairs/groups, testing combinations, or solving combinatorial problems. In Python, nested loops with careful indexing let you build combinations manually, but for larger sizes or more combinations, this becomes slow, memory-heavy, and error-prone. In 2026, while loops teach the concept, itertools.combinations() is the go-to for efficiency and readability — it’s lazy, memory-safe, and implemented in C for speed.
Here’s a complete, practical guide to generating combinations with loops and beyond: manual nested loops, common patterns, performance limits, itertools.combinations(), real-world uses, and modern best practices with type hints and scalability.
Manual nested loops use range() with increasing start indices to avoid duplicates and respect order-independence.
lst = [1, 2, 3]
for i in range(len(lst)):
for j in range(i + 1, len(lst)):
print(lst[i], lst[j])
# Output:
# 1 2
# 1 3
# 2 3
Extend to 3-item combinations with three nested loops — indices always increase to avoid repeats.
lst = [1, 2, 3, 4]
for i in range(len(lst)):
for j in range(i + 1, len(lst)):
for k in range(j + 1, len(lst)):
print(lst[i], lst[j], lst[k])
# Output:
# 1 2 3
# 1 2 4
# 1 3 4
# 2 3 4
Real-world pattern: generating pairs or groups for testing, matching, or combinatorial search — loops work for small n, but explode in time/memory for larger n (O(n choose k) grows fast).
# Find all pairs of users with similar scores (small n)
users = [("Alice", 85), ("Bob", 92), ("Charlie", 78)]
for i in range(len(users)):
for j in range(i + 1, len(users)):
name1, score1 = users[i]
name2, score2 = users[j]
if abs(score1 - score2) <= 10:
print(f"{name1} and {name2} have similar scores")
For efficiency, use itertools.combinations() — it generates tuples lazily, uses no extra memory beyond one combo at a time, and is much faster than manual loops for moderate sizes.
import itertools
lst = [1, 2, 3, 4]
for combo in itertools.combinations(lst, 2):
print(combo)
# Output: same pairs as manual loop, but cleaner and faster
# (1, 2) (1, 3) (1, 4) (2, 3) (2, 4) (3, 4)
# With larger r or n — still memory-safe
print(list(itertools.combinations(range(20), 3))) # 1140 combinations, no problem
Best practices make combination generation efficient and readable. Prefer itertools.combinations() over manual nested loops — it’s clearer, faster (C-level), and lazy (constant memory). Use combinations() when order doesn’t matter — use permutations() when it does. Add type hints for clarity — Iterator[tuple[int, ...]] — improves readability and mypy checks. Modern tip: use Polars for large combinatorial data — pl.DataFrame(...).join(..., how="cross") or pl.int_range(...).join(...) for Cartesian products. In production, wrap combination generation over external data (files, APIs) in try/except — handle bad items gracefully. Avoid materializing large combinations — iterate lazily with for combo in combinations(...) or use islice() to take only first n. Combine with enumerate() for indexed combinations — for i, combo in enumerate(combinations(lst, r), start=1).
Generating combinations with loops (or better, itertools.combinations()) unlocks combinatorial power — essential for subsets, pairs, testing, and search. In 2026, use itertools for efficiency, keep loops for learning or tiny cases, and add type hints for safety. Master combinations, and you’ll solve grouping, matching, and selection problems with speed and elegance.
Next time you need all possible selections — reach for itertools.combinations(). It’s Python’s cleanest way to say: “Give me every unique group, no repeats, no order.”