PythonMastery
beginner 16 min read · lesson 11 of 19 in Python Fundamentals

Loops: for, while, and the Pythonic Patterns

1 · The lesson

read

Computers earn their keep doing the same thing thousands of times — checking every row in a file, every pixel in an image, every user in a database. Loops are how you express that. Python gives you two — for and while — and a small kit of helpers (enumerate, zip, range) that separate idiomatic code from beginner code.


1. The for Loop

A for loop walks over any iterable — a list, a string, a dictionary, a range, anything you can ask for the next item of.

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

for fruit in fruits:
    print(fruit)

# Strings are iterables of characters
for char in "hello":
    print(char)

# Dicts iterate over keys by default
prices = {"apple": 1.20, "banana": 0.40}
for key in prices:
    print(key, prices[key])

The loop variable (fruit, char, key) is just a regular variable — pick a meaningful name. After the loop, it keeps its last value.


2. range() — Counting

range produces integers on demand without building a list in memory.

python
for i in range(5):              # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 7):           # 2, 3, 4, 5, 6  — stop is exclusive
    print(i)

for i in range(0, 20, 5):       # 0, 5, 10, 15   — step parameter
    print(i)

for i in range(10, 0, -1):      # countdown: 10, 9, 8, ..., 1
    print(i)

range(stop) starts at 0. range(start, stop) excludes stop. range(start, stop, step) lets you skip or count backwards.


3. The while Loop

for is for iterating a known collection. while is for repeating until a condition stops being true.

python
balance = 1000
days = 0

while balance < 2000:
    balance *= 1.05             # 5% growth per day
    days += 1

print(f"Doubled in {days} days.")

Use while when you don't know in advance how many iterations you need — user input, convergence loops, retries, simulations. Use for when you have a collection or a range.


4. break and continue

break exits the loop immediately. continue skips to the next iteration.

python
# Find the first negative number
numbers = [4, 7, 12, -3, 5, -8]

for n in numbers:
    if n < 0:
        print(f"First negative: {n}")
        break                   # done — stop looking

# Skip odd numbers
for n in range(10):
    if n % 2 == 1:
        continue                # skip the rest of this iteration
    print(n)                    # prints 0, 2, 4, 6, 8

continue is less common than break, and overusing it makes loops hard to read. If you find yourself with three continues, restructure the condition.


5. The Curious for...else

Python's loops have an else clause that runs only if the loop completed without hitting break. The classic use is search:

python
numbers = [4, 7, 12, 8, 5]
target = 9

for n in numbers:
    if n == target:
        print(f"Found {target}.")
        break
else:
    print(f"{target} not found.")     # runs because no break happened

Without for...else, you'd need a found = False flag. With it, the "not found" branch is right there next to the loop. Same idea works for while...else. It's an obscure feature — recognise it, use it sparingly.


6. enumerate — Index and Value

If you need both the position and the item, don't reach for range(len(...)). Use enumerate:

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

for i, fruit in enumerate(fruits):
    print(f"{i}: {fruit}")
# 0: apple
# 1: banana
# 2: cherry

# Start counting from a different number
for i, fruit in enumerate(fruits, start=1):
    print(f"{i}. {fruit}")
# 1. apple
# 2. banana
# 3. cherry

enumerate is the canonical Python idiom for "I need the index too". Writing for i in range(len(fruits)): and then fruits[i] everywhere marks code as written by someone who learned loops in another language first.


7. zip — Walking Two (or More) Iterables Together

python
names = ["Alice", "Bob", "Charlie"]
ages = [30, 25, 35]

for name, age in zip(names, ages):
    print(f"{name} is {age}")

# Three or more works the same way
cities = ["NYC", "LA", "SF"]
for name, age, city in zip(names, ages, cities):
    print(f"{name} ({age}) — {city}")

If the iterables have different lengths, zip stops at the shortest. No error, no warning — silent truncation:

python
short = [1, 2]
long = [10, 20, 30, 40]
print(list(zip(short, long)))       # [(1, 10), (2, 20)]

If you'd rather get an error on mismatch, use zip(..., strict=True) (Python 3.10+).


8. reversed and sorted in Loops

python
# Iterate backwards without building a reversed list
for n in reversed(range(1, 6)):
    print(n)                        # 5, 4, 3, 2, 1

# Iterate in sorted order without modifying the original
scores = [42, 17, 89, 33]
for s in sorted(scores):
    print(s)                        # 17, 33, 42, 89

for s in sorted(scores, reverse=True):
    print(s)                        # 89, 42, 33, 17

Both return iterators, so you don't pay the memory cost of an extra list unless you wrap them with list().


9. Nested Loops

A loop inside a loop. Useful — and a complexity multiplier.

python
# Multiplication table
for row in range(1, 4):
    for col in range(1, 4):
        print(f"{row * col:3d}", end=" ")
    print()                         # newline between rows

If the outer loop runs n times and the inner runs n times, that's n² total iterations — O(n²) time complexity. Fine for small n. For n = 10_000, you're at 100 million iterations and your program is suddenly slow. Whenever you nest, ask: is there a flatter way? Often a dict lookup or a set membership test (both O(1)) replaces an inner loop.


10. List Comprehensions — A Teaser

When the loop's only job is to build a new list, Python has a one-line form:

python
# The long way
doubled = []
for x in range(5):
    doubled.append(x * 2)

# The Pythonic way
doubled = [x * 2 for x in range(5)]     # [0, 2, 4, 6, 8]

# With a filter
evens = [x for x in range(20) if x % 2 == 0]

There's a whole lesson on this pattern — see comprehensions. For now, recognise the shape: [expression for item in iterable if condition].


Common Mistakes

1. Modifying a list while iterating it

python
numbers = [1, 2, 3, 4, 5]

# Bug: items get skipped because indices shift
for n in numbers:
    if n % 2 == 0:
        numbers.remove(n)
print(numbers)                  # [1, 3, 5] — looks fine but fragile

# Fix A: iterate over a copy
for n in numbers[:]:            # the [:] makes a shallow copy
    if n % 2 == 0:
        numbers.remove(n)

# Fix B (better): build a new list
numbers = [n for n in numbers if n % 2 != 0]

The second form is faster, clearer, and immune to indexing weirdness.

2. range(len(x)) when you wanted enumerate(x)

python
items = ["a", "b", "c"]

# Code smell — flags you as a non-Pythonista
for i in range(len(items)):
    print(i, items[i])

# Pythonic
for i, item in enumerate(items):
    print(i, item)

This is the most common loop anti-pattern beginners write. Burn it out early.

3. Forgetting to update the loop variable in while

python
i = 0
while i < 5:
    print(i)
    # forgot to increment i — infinite loop!

If your program hangs, the first place to look is a while whose condition never changes. Hit Ctrl+C to stop it, then add the missing update.

4. break only exits the innermost loop

python
# Searching a 2D grid for a value
grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
target = 5

for row in grid:
    for cell in row:
        if cell == target:
            print("Found!")
            break               # only breaks the inner loop
    # ...outer loop keeps going

# Fix: refactor into a function and use return
def find(grid, target):
    for row in grid:
        for cell in row:
            if cell == target:
                return True     # exits the whole function
    return False

Promoting nested loops into a function with an early return is almost always cleaner than a found flag.

5. Off-by-one in range

python
# "I want 1 through 10"
for i in range(1, 10):
    print(i)                    # prints 1..9 — missed 10!

# Correct
for i in range(1, 11):
    print(i)                    # 1..10

range(start, stop) is half-open — stop is excluded. If you want N items including N, write range(1, N + 1) or use range(N) and shift in your head.


🎯 Your Turn — FizzBuzz

The interview classic. Write fizzbuzz(n) that prints the numbers from 1 to n, with these rules:

  • Multiples of 3 → print "Fizz"
  • Multiples of 5 → print "Buzz"
  • Multiples of both (i.e. 15) → print "FizzBuzz"
  • Anything else → print the number itself

Skeleton:

python
def fizzbuzz(n):
    # TODO: loop from 1 to n inclusive
    # TODO: decide Fizz / Buzz / FizzBuzz / number
    ...

fizzbuzz(20)
# 1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz,
# 11, Fizz, 13, 14, FizzBuzz, 16, 17, Fizz, 19, Buzz
Hint 1 — The range Remember range(1, n + 1) to get 1 through n inclusive. range(1, n) stops at n - 1.
Hint 2 — Check the most specific case first Order matters. 15 is divisible by both 3 and 5 — if you check i % 3 == 0 first, you'll print "Fizz" and miss "FizzBuzz". Put the combined check (divisible by 15, or divisible by 3 and 5) at the top of the chain.
Show full solution
python
def fizzbuzz(n):
    for i in range(1, n + 1):
        if i % 15 == 0:                 # most specific case first
            print("FizzBuzz")
        elif i % 3 == 0:
            print("Fizz")
        elif i % 5 == 0:
            print("Buzz")
        else:
            print(i)


fizzbuzz(20)

An even slicker variant using string concatenation — no nested condition, the "FizzBuzz" case falls out naturally:

python
def fizzbuzz(n):
    for i in range(1, n + 1):
        output = ""
        if i % 3 == 0:
            output += "Fizz"
        if i % 5 == 0:
            output += "Buzz"
        print(output or i)              # truthiness: "" is falsy, so print i

The output or i trick uses falsy strings — if output is "" (neither rule fired), Python falls through to i. Two ifs, no elif chain, no special case for 15. This is the kind of solution that makes interviewers smile.


What You Learned

  • for for iterables (lists, strings, dicts, ranges). while for "until a condition changes".
  • range(start, stop, step) — stop is exclusive. range(N) starts at 0.
  • break exits, continue skips to the next iteration.
  • for...else — runs only if no break happened. Great for search loops.
  • enumerate(items) — the right way to get index + value. Never range(len(items)).
  • zip(a, b) — parallel iteration, stops at the shortest. Use strict=True to enforce equal length.
  • reversed and sorted return iterators — cheap to use directly in a for.
  • Nested loops are O(n²) — flatten with dicts/sets where you can.
  • List comprehensions are the one-line for for building lists.
  • Don't mutate a list while iterating it. Build a new one with a comprehension instead.

Next: Functions — wrapping your loops and conditions into reusable, named units.

Practice this

on practicepython.in

Short exercises that run in your browser and tell you what your code actually did, not just whether a test passed.