Python For Loops: Iteration with Examples

Python's for loop is fundamentally different from what you'll find in C, Java, or JavaScript. Instead of manually managing a counter variable, Python's for loop iterates directly over elements in a...

Key Insights

  • Python’s for loops iterate over any iterable object, making them more flexible than traditional counter-based loops in languages like C or Java
  • The range(), enumerate(), and zip() functions transform how you write loops, eliminating the need for manual index tracking in most cases
  • Understanding when to use list comprehensions versus explicit for loops leads to more readable and performant Python code

Introduction to For Loops

Python’s for loop is fundamentally different from what you’ll find in C, Java, or JavaScript. Instead of manually managing a counter variable, Python’s for loop iterates directly over elements in a sequence. This design choice makes code more readable and less error-prone.

The basic syntax is straightforward:

for item in iterable:
    # do something with item
    pass

Use for loops when you know you need to iterate over a collection or perform an action a specific number of times. They’re your default choice for iteration in Python. Reserve while loops for situations where you’re waiting for a condition to change and don’t know how many iterations you’ll need.

Here’s a simple example:

numbers = [1, 2, 3, 4, 5]
for num in numbers:
    print(num * 2)
# Output: 2, 4, 6, 8, 10

Notice there’s no index management, no i++, and no risk of off-by-one errors. Python handles the iteration mechanics for you.

Iterating Over Different Data Types

Python’s for loops work with any iterable object. This flexibility is one of Python’s greatest strengths.

Lists and Tuples work identically for iteration purposes:

cities = ['Tokyo', 'London', 'Paris']
for city in cities:
    print(f"Visiting {city}")

coordinates = (40.7128, -74.0060)  # tuple
for coord in coordinates:
    print(coord)

Strings are iterable character sequences:

word = "Python"
for char in word:
    print(char.upper())
# Output: P, Y, T, H, O, N

Dictionaries offer three iteration methods:

user = {'name': 'Alice', 'age': 30, 'city': 'NYC'}

# Iterate over keys (default behavior)
for key in user:
    print(key)

# Iterate over values
for value in user.values():
    print(value)

# Iterate over key-value pairs (most useful)
for key, value in user.items():
    print(f"{key}: {value}")

Always use .items() when you need both keys and values. It’s more efficient and readable than accessing dict[key] inside the loop.

Sets iterate in arbitrary order since they’re unordered:

tags = {'python', 'tutorial', 'loops'}
for tag in tags:
    print(tag.capitalize())

The range() Function

The range() function generates numeric sequences. It’s lazy—it doesn’t create a list in memory but generates numbers on-demand.

Basic counting with range(stop):

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

Note that range(5) produces 0 through 4, not 1 through 5. Python uses zero-based indexing everywhere.

Custom start and stop with range(start, stop):

for i in range(10, 15):
    print(i)
# Output: 10, 11, 12, 13, 14

Reverse iteration with negative step:

for i in range(10, 0, -1):
    print(i)
# Output: 10, 9, 8, 7, 6, 5, 4, 3, 2, 1

Index-based iteration using range(len()):

items = ['a', 'b', 'c']
for i in range(len(items)):
    print(f"Index {i}: {items[i]}")

However, this pattern is usually a code smell in Python. Use enumerate() instead (covered later).

Loop Control: break, continue, and else

Python provides three keywords to control loop execution flow.

break exits the loop immediately:

def find_first_negative(numbers):
    for num in numbers:
        if num < 0:
            print(f"Found negative number: {num}")
            break
        print(f"Checking {num}")

find_first_negative([5, 3, -2, 8, -1])
# Output:
# Checking 5
# Checking 3
# Found negative number: -2

continue skips the rest of the current iteration:

for num in range(1, 6):
    if num == 3:
        continue
    print(num)
# Output: 1, 2, 4, 5

for-else executes when the loop completes normally (without hitting break):

def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    else:
        return True  # Loop completed without finding divisor

print(is_prime(17))  # True
print(is_prime(18))  # False

The for-else pattern is perfect for search operations where you need to know if you found what you were looking for or exhausted all possibilities.

Advanced Iteration Techniques

These built-in functions make your loops more Pythonic and often more performant.

enumerate() provides index-value pairs:

fruits = ['apple', 'banana', 'cherry']
for index, fruit in fruits:
    print(f"{index}: {fruit}")
# Output:
# 0: apple
# 1: banana
# 2: cherry

# Start counting from 1
for index, fruit in enumerate(fruits, start=1):
    print(f"{index}. {fruit}")

This eliminates the need for range(len()) in almost all cases.

zip() iterates over multiple sequences in parallel:

names = ['Alice', 'Bob', 'Charlie']
scores = [85, 92, 78]
grades = ['B', 'A', 'C']

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

If sequences have different lengths, zip() stops at the shortest. Use itertools.zip_longest() if you need different behavior.

List comprehensions replace simple for loops:

# Instead of:
squares = []
for i in range(10):
    squares.append(i ** 2)

# Write:
squares = [i ** 2 for i in range(10)]

# With filtering:
even_squares = [i ** 2 for i in range(10) if i % 2 == 0]

Use list comprehensions for simple transformations and filters. If the logic gets complex or you need multiple statements, stick with explicit loops.

Nested loops handle multi-dimensional data:

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

for row in matrix:
    for value in row:
        print(value, end=' ')
    print()  # newline after each row

Common Pitfalls and Best Practices

Never modify a list while iterating over it:

# WRONG - skips elements
numbers = [1, 2, 3, 4, 5]
for num in numbers:
    if num % 2 == 0:
        numbers.remove(num)

# RIGHT - iterate over a copy
numbers = [1, 2, 3, 4, 5]
for num in numbers[:]:  # [:] creates a shallow copy
    if num % 2 == 0:
        numbers.remove(num)

# BETTER - use list comprehension
numbers = [num for num in numbers if num % 2 != 0]

Understand iterator vs materialized list performance:

# Creates a list in memory (avoid for large ranges)
sum([i ** 2 for i in range(1000000)])

# Uses generator expression (memory efficient)
sum(i ** 2 for i in range(1000000))

For large datasets, generators and range() objects are more memory-efficient than creating full lists.

Use while loops for condition-based iteration:

# AWKWARD with for loop
for _ in range(1000000):
    if user_input_valid():
        break
    user_input = get_user_input()

# CLEAR with while loop
while not user_input_valid():
    user_input = get_user_input()

If you don’t know how many iterations you need, or you’re waiting for a condition to change, use a while loop.

Conclusion

Python’s for loops are powerful precisely because they’re simple. By iterating directly over objects rather than managing indices, you write clearer code with fewer bugs. Master range(), enumerate(), and zip(), and you’ll handle 95% of iteration scenarios elegantly.

The key is choosing the right tool: use for loops for definite iteration over collections, list comprehensions for simple transformations, and while loops when you’re waiting for conditions to change. Practice these patterns with real data, and they’ll become second nature.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.