AlgoMaster Logo

Nested Loops

High Priority20 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

A loop whose body contains another loop is called a nested loop. Walking every cell in a grid, comparing every pair of items in a wishlist, generating a price table across products and quantities, all of these need iteration inside iteration. The shape is simple, but the cost grows fast, and mutation and early exit have specific pitfalls. This lesson covers when nesting is the right shape, how to control it, and the cases where a different data structure replaces one of the loops entirely.

The Basic Shape

A nested loop is exactly what the name says: a for (or while) loop sitting inside another for (or while) loop. The inner loop runs from start to finish for every single pass of the outer loop.

The outer loop binds x to 0, then 1, then 2. Each time x changes, the inner loop runs fully from y = 0 to y = 1. Three outer values times two inner values gives six lines of output, and the order is "outer first, inner fast".

Indentation tells Python which body belongs to which loop. The inner for and its print are both indented one level deeper than the outer for. Misaligning them by even one space changes the meaning of the program (and often produces an IndentationError).

An e-commerce example: an order is a list of items, and a list of orders contains many of these. Walking every item in every order is a two-level iteration.

The outer loop walks the three orders. For each order, the inner loop walks the items inside that order. The body of the outer loop has two statements: the print of the order header, and the inner for loop. Both run in order for every outer iteration.

The diagram shows the control flow. The inner loop fully drains for each outer value, then the outer loop advances and the inner loop starts over from the beginning. Two nested loops over collections of size N do N times N iterations.

When To Use Nested Loops

Nesting isn't an end in itself. Use it when the problem genuinely involves two layers of structure. Four cases come up often.

Walking a 2D Grid

A grid is a list of lists, where each inner list is one row. To touch every cell, walk rows in the outer loop and columns in the inner loop.

The outer loop pulls one row at a time. The inner loop walks the products in that row and prints them on the same line, separated by spaces. The print() after the inner loop adds a newline at the end of each row.

For the position of each cell (row index and column index), enumerate works on both layers:

Each cell gets its row-column pair printed alongside its value. This is the standard pattern for matrix-style work: image pixels, spreadsheet cells, board positions in a game.

Building a 2D Structure

The reverse of walking a grid is building one. A double loop creates rows and fills each row with values.

The outer loop builds one row per product. The inner loop fills that row with one entry per quantity. The result is a list of lists with shape three by three, ready to be iterated, indexed, or formatted as a table.

Pair Generation

Some tasks require every pairing of items from two collections (or from one collection paired with itself). A nested loop over products and quantities produces every (product, quantity) combination.

Two products and three quantities give six combinations. The blank print() after the inner loop adds a separator between groups.

To find pairs of items in one list that satisfy some condition, a double loop checks every pair. A common case: find every pair of products in a catalog whose combined price hits exactly a target value (a bundle deal).

The i < j guard avoids checking the same pair twice and skips comparing an item with itself. This is the standard "two nested loops over the same list" shape, and it's O(N squared). For five prices, that's 25 iterations, which is fine. For a million prices, it's a trillion, which is not.

Two nested loops over a collection of size N do roughly N times N iterations. For N = 1,000, that's a million. For N = 1,000,000, that's a trillion. When the inner loop is doing a lookup or a membership check, a set or dict often collapses it down to one O(1) step, turning O(N squared) into O(N).

Nested while Loops

The same nesting idea works with while. The shape comes up when neither loop has a fixed iterable, and both depend on conditions.

The outer while controls the row counter, the inner while controls the column counter. Each counter has to be advanced inside its own loop body, and the inner counter has to be reset before the inner loop starts each time.

The reset matters. Without col = 0 before the inner loop, col keeps its final value from the previous iteration, and the inner loop won't run again because the condition col < 3 is already false. The outer loop will run forever after row 0, printing nothing.

Loop types can also be mixed. A for outside a while is common when the outer iteration is over a collection but the inner step depends on a running condition:

This is a contrived example because for item in order would be cleaner. The nesting shape works the same way regardless of which loop kind is on which level.

Breaking Out of Nested Loops

break only exits the innermost loop it sits inside. This is the most common source of confusion when both loops should stop at once.

The break exits the inner loop the moment it finds 99, but the outer loop keeps going. When the intent is "stop everything", that's a bug. Python has no labeled break, so the fix uses one of three workarounds.

Flag Variable

Set a flag inside the inner loop and check it after the inner loop ends.

Simple and explicit. The cost is the two-line check after the inner loop, and the extra variable to manage. For deeply nested loops, the flag-and-check pattern stacks up and becomes clumsy.

Wrap in a Function and Return

Returning from a function exits every loop inside it at once. This is often the cleanest fix.

return acts as a labeled break that also unwinds the call stack. When the loops naturally belong inside a search or filter function, this is the most readable approach.

Raise an Exception (and Catch It)

An alternative is to raise a custom exception and catch it after the loops. It's overkill for normal cases but useful when the exit condition is deeply buried inside complex logic.

Most Python code prefers the function-and-return approach. Exceptions are heavier and harder to read than a flag, but they help when the loops are buried inside helper code where introducing a function would be a bigger refactor.

Flatten with itertools.product

When the only reason for nesting is to walk every combination, itertools.product flattens both loops into one. A single break then exits the whole thing.

product(products, quantities) yields every (name, q) pair in the same order the two for loops would. Because the outer iteration is now a single flat loop, one break exits both. itertools is covered properly in the iterators-and-generators section; for now, treat it as a useful one-liner.

Continue in Nested Loops

continue also only affects the innermost loop. It skips the rest of the current inner iteration and moves to the next one, without touching the outer loop.

continue skips the print(item) line when item is None, but the inner loop keeps running for the rest of that order. The outer loop sees no change.

Modifying Lists During Nested Iteration

The same rule from the for loop chapter applies, twice as hard for nested loops: don't change a list during iteration. If both loops walk the same list and the inner body mutates, the bugs become hard to spot.

What's wrong with this code?

The outer loop walks prices. The inner loop walks the same prices. Removing an item shifts every later element down by one, which corrupts both loops' internal indices. The next removal might try to delete a value that's already gone, raising ValueError. Even when it doesn't crash, the results are wrong.

The fix is the same as before: iterate over a copy, or build a new list of the items to keep.

The nested loops only read from prices. The mutation happens after both loops finish, on a fresh list. The i < j guard avoids counting each pair twice.

A nested loop that reads from a list is fine. A nested loop that mutates the list it's reading is almost always wrong. To mutate, gather what to change in a separate collection during the loops, then apply the changes once after.

Replacing the Inner Loop With a Set or Dict

The most useful insight about nested loops is that the inner loop is often doing a lookup. Building a set or dict once before the outer loop collapses the inner loop into a single O(1) check.

A practical case: checking which wishlist items are in stock. The naive nested-loop version walks the whole stock list for every wishlist item.

This is O(wishlist times in_stock). Converting in_stock to a set makes the inner check O(1) and the inner loop goes away entirely:

Same answer, single loop, much faster. Building the set is a one-time O(N) cost that pays itself back many times over with repeated lookups.

A dictionary works the same way when both membership and a value tied to each key are needed. When the inner loop searches for a stock count by product name, a dict from name to count replaces it cleanly:

stock.get(wanted, 0) is one O(1) lookup. The nested-loop version would scan the entire stock list for every wishlist entry. For a thousand products, that's a thousand times less work.

The rule isn't "never nest loops". It's "when nesting, ask whether one of the loops is doing a lookup that a set or dict could handle in one step".

Three or More Levels of Nesting

Loops can nest as deep as needed, but the cost grows multiplicatively at every level. Three nested loops over collections of size N do N cubed iterations. Four levels do N to the fourth. Even N = 100 at three levels is a million iterations, which is the upper end of "still fast enough".

Eight combinations, all of them legitimate. The shape is fine when the levels represent different dimensions (category, product, quantity).

Deep nesting becomes a smell when the loops are searching, filtering, or matching the same kind of data. Three nested loops looking for a triple that sums to a target value is the classic case, and there's almost always a smarter algorithm (sort first, then use two pointers) that brings it back to a single layer plus a fast lookup.

Three nested loops over collections of size 1,000 do a billion iterations. Even at one nanosecond per iteration, that's a full second of work. Watch the sizes when going past two levels, and use a set, a dict, or a sort-based technique before accepting the cubed cost.

Inner Loop Variable Survives the Outer Loop

A trap that's easy to miss: the inner loop's variable is still in scope after the inner loop ends, and after the outer loop ends. It holds whatever value it was last bound to.

row is the last list the outer loop visited. value is the last element of that last row. This is the same scoping rule as a single for loop: variables don't get cleaned up because the loop ended.

If the inner iterable is ever empty, the inner variable might not exist at all. Using it after that raises NameError. This rarely matters in practice, but the variables do stick around.

Quiz

Nested Loops Quiz

10 quizzes