Loops in Python: Complete For, While, and Nested Loop Structures and Iteration Implementation Guide

Loops in python

Loops were one of the first things I learned in Python, and honestly, one of the last things I truly mastered. It’s easy to write a basic for loop early on, but understanding how Python’s iteration model actually works underneath — and knowing when to reach for a while loop instead, or how to structure nested loops without tanking performance — took real practice. Here’s everything I’ve picked up along the way.

The for Loop

Python’s for loop is fundamentally different from the C-style for (i = 0; i < n; i++) loop many people learn first. Python’s version is really a for-each loop — it iterates over the elements of any iterable directly.

fruits = ["apple", "banana", "cherry"]

for fruit in fruits:
    print(fruit)

Output:

apple
banana
cherry

Looping With range()

When I need index-based iteration, I use range():

for i in range(5):
    print(i)

Output:

0
1
2
3
4

range() supports start, stop, and step arguments too:

for i in range(2, 10, 2):
    print(i, end=" ")

Output:

2 4 6 8

Looping With enumerate()

When I need both the index and the value, I reach for enumerate() instead of manually tracking a counter:

colors = ["red", "green", "blue"]

for index, color in enumerate(colors):
    print(index, color)

Output:

0 red
1 green
2 blue

The while Loop

A while loop keeps executing as long as a condition remains true. I use it when I don’t know in advance how many iterations I’ll need.

count = 0
while count < 5:
    print(count)
    count += 1

Output:

0
1
2
3
4

while True and Controlled Exit

For loops that should run indefinitely until an internal condition is met, I use while True combined with break:

count = 0
while True:
    print(count)
    count += 1
    if count >= 5:
        break

Output:

0
1
2
3
4

I use this pattern often for things like reading input until a sentinel value appears, or polling a resource until it’s ready.

Internal Working: How Python Executes Iteration

This part genuinely changed how I think about loops. When I write for item in my_list:, Python doesn’t index into the list manually behind the scenes. Instead, it calls iter(my_list) to get an iterator object, and then repeatedly calls next() on that iterator until it raises a StopIteration exception, which the for loop catches internally to know when to stop.

my_list = [10, 20, 30]
it = iter(my_list)

print(next(it))
print(next(it))
print(next(it))
print(next(it))  # raises StopIteration

Output:

10
20
30
Traceback (most recent call last):
  ...
StopIteration

This is why any object implementing the iterator protocol (__iter__ and __next__) can be used in a for loop, not just lists — this includes generators, file objects, dictionary views, and custom classes.

break, continue, and else in Loops

for i in range(10):
    if i == 5:
        break
    print(i, end=" ")

Output:

0 1 2 3 4
for i in range(10):
    if i % 2 == 0:
        continue
    print(i, end=" ")

Output:

1 3 5 7 9

One feature that confused me for a long time was the loop else clause. The else block runs only if the loop completes without hitting a break:

for i in range(5):
    if i == 10:
        break
else:
    print("Loop completed without break")

Output:

Loop completed without break

I use this pattern occasionally for search-style loops, where the else clause signals “nothing was found.”

Nested Loops

Nested loops are loops inside other loops, useful for working with grid-like or multi-dimensional data.

for i in range(3):
    for j in range(3):
        print(f"({i}, {j})", end=" ")
    print()

Output:

(0, 0) (0, 1) (0, 2)
(1, 0) (1, 1) (1, 2)
(2, 0) (2, 1) (2, 2)

Breaking Out of Nested Loops

break only exits the innermost loop, which trips people up regularly:

for i in range(3):
    for j in range(3):
        if j == 1:
            break
        print(f"({i}, {j})", end=" ")

Output:

(0, 0) (1, 0) (2, 0)

To break out of multiple nested loops at once, I typically use a flag variable or restructure the logic into a function and return early:

def find_pair(target, rows):
    for i in range(len(rows)):
        for j in range(len(rows[i])):
            if rows[i][j] == target:
                return i, j
    return None

grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(find_pair(5, grid))

Output:

(1, 1)

Performance and Complexity Considerations

Nested loops multiply their complexity — a loop of size n inside another loop of size n runs in O(n²) time. I’ve been burned by this before when processing what I assumed was a small dataset that turned out to have nested loops scaling quadratically once the data grew.

import time

data = list(range(2000))

start = time.time()
count = 0
for i in data:
    for j in data:
        count += 1
print("Nested loop time:", time.time() - start)

For large datasets, I always ask myself whether a nested loop is really necessary, or whether a dictionary lookup, set intersection, or vectorized approach (like with NumPy) could replace an O(n²) operation with something closer to O(n) or O(n log n).

List Comprehensions as a Loop Alternative

For simple transformations, I often replace explicit for loops with list comprehensions, which are typically faster because the looping happens at the C level inside CPython rather than through repeated Python bytecode execution for an explicit loop:

squares = [x**2 for x in range(10)]
print(squares)

Output:

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Common Mistakes

  1. Modifying a list while iterating over it — This leads to skipped elements or unexpected behavior. I always iterate over a copy (for item in my_list[:]) if I need to modify the original during iteration.
  2. Off-by-one errors with range() — Remembering that range(n) stops before n trips up beginners constantly.
  3. Infinite while loops from a forgotten increment — Always double-check that the loop’s condition variable actually changes inside the loop body.
  4. Overusing nested loops when a dictionary or set would be faster — Searching for matches with nested loops is often an O(n²) operation that a hash-based lookup can reduce to roughly O(n).
  5. Misunderstanding break inside nested loops — As shown above, break only escapes the innermost loop.

Debugging Tips

When a loop isn’t behaving as expected, I add temporary print statements showing the loop variable and any relevant state at each iteration:

for i, value in enumerate(my_list):
    print(f"Iteration {i}: value={value}")

For infinite loop suspicions, I add a hard safety cap during debugging:

count = 0
while some_condition:
    count += 1
    if count > 10000:
        print("Possible infinite loop detected")
        break

Real-World Applications

  • Data processing pipelines: Iterating over rows of data to clean, transform, or validate them.
  • Polling and retries: while loops checking a condition (like an API becoming available) with a timeout.
  • Grid and matrix operations: Nested loops for image processing, game boards, or spreadsheet-like data.
  • Batch automation scripts: Looping over files in a directory to perform repetitive tasks.
  • Search algorithms: Nested loops for brute-force search, later optimized with better data structures once correctness is confirmed.

Looping Over Multiple Sequences With zip()

A pattern I use constantly is looping over two or more sequences in parallel using zip(), rather than indexing into each one manually:

names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]

for name, score in zip(names, scores):
    print(f"{name}: {score}")

Output:

Alice: 85
Bob: 92
Charlie: 78

zip() stops as soon as the shortest input sequence is exhausted, which is worth remembering if the sequences aren’t the same length — I sometimes use itertools.zip_longest() instead when I need to pad the shorter sequence rather than truncate the result.

The walrus Operator in Loop Conditions

Since Python 3.8, the walrus operator (:=) lets me assign and check a value in the same expression, which is particularly handy in while loop conditions:

import random

results = []
while (value := random.randint(1, 6)) != 6:
    results.append(value)

print("Rolled until a 6:", results)

This avoids the slightly awkward pattern of assigning a value before the loop and then reassigning it at the end of the loop body just to keep the condition check current.

FAQs

What’s the difference between a for loop and a while loop? A for loop iterates over a known iterable or a defined range, while a while loop continues based on a condition, useful when the number of iterations isn’t known in advance.

Can I use else with a while loop? Yes — just like for loops, while loops support an else clause that runs only if the loop completes without a break.

Is a list comprehension always faster than a for loop? For simple transformations, generally yes, because comprehensions avoid some of the Python-level overhead of an explicit loop, though the difference becomes less relevant for complex loop bodies.

How do I loop through a dictionary? Use .items(), .keys(), or .values() depending on what you need: for key, value in my_dict.items():.

Summary

Loops are the backbone of iteration in Python, and understanding the distinction between for and while, how the iterator protocol powers for loops under the hood, and the performance implications of nesting loops has made a real difference in how I write efficient, readable code. Mastering break, continue, and the lesser-known loop else clause rounds out a solid foundation for handling almost any iteration scenario.

References

Total
0
Shares

Leave a Reply

Previous Post
Comparison operators in python

Comparison Operators in Python: Complete Equality, Relational, and Logical Comparison Implementation Guide

Next Post
Iterating different portion of a list with different step size in python

Iterating Different Portion of a List with Different Step Size in Python: Complete List Slicing and Iteration Guide

Related Posts