for Loops in Python

Almost anything that needs to happen repeatedly — processing every item in a list, reading every line of a file, checking every character in a string — comes down to a python for loop. This article covers how Python's for loop actually works (it's a bit different from what you might expect coming from other languages), how to loop through the data types you already know, and the patterns that separate readable Python loops from clunky ones.

Introduction: What Is a for Loop?

A for loop repeats a block of code once for every item in a sequence — a list, a string, a tuple, or any other iterable.

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

for fruit in fruits:
    print(fruit)
How this differs from C-style loops

If you've used a language like C, Java, or JavaScript, you might expect a for loop to involve manually managing a counter variable and an index — something like for (int i = 0; i < length; i++). Python's for loop doesn't work that way at all. It's closer to what other languages call a "foreach" loop: you don't track an index yourself, you simply get handed each item in the sequence, one at a time, until there are none left.

Basic syntax
for variable in sequence:
    # code that runs once per item, with 'variable' set to the current item

Iterating Over Common Data Types

Lists
numbers = [10, 20, 30]

for number in numbers:
    print(number * 2)
# 20
# 40
# 60
Strings

Looping over a string iterates character by character:

for letter in "Python":
    print(letter)
# P
# y
# t
# h
# o
# n
Tuples
coordinates = (3, 7, 12)

for value in coordinates:
    print(value)
Dictionaries

Looping over a dictionary directly iterates its keys by default:

user = {"name": "Alex", "age": 30}

for key in user:
    print(key)
# name
# age

To get both keys and values together, use .items():

for key, value in user.items():
    print(f"{key}: {value}")
# name: Alex
# age: 30

This .items() pattern is one you'll use constantly once you start working with dictionaries in real code.

A note on iteration order

Iteration always follows the sequence's definition or insertion order — for lists and tuples, that's simply the order the items appear in; for dictionaries, Python guarantees insertion order is preserved (this has been true since Python 3.7). You don't need to worry about items appearing in some unpredictable order.

Using range() for Numeric Loops

Sometimes you don't want to loop over an existing collection — you just want to repeat something a specific number of times, or generate a sequence of numbers. That's what range() is for.

The three forms
# range(stop) — generates numbers from 0 up to (not including) stop
for i in range(5):
    print(i)
# 0 1 2 3 4

# range(start, stop) — generates numbers from start up to (not including) stop
for i in range(2, 6):
    print(i)
# 2 3 4 5

# range(start, stop, step) — generates numbers from start to stop, skipping by step
for i in range(0, 10, 2):
    print(i)
# 0 2 4 6 8
Looping backward

A negative step counts downward instead of upward:

for i in range(10, 0, -1):
    print(i)
# 10 9 8 7 6 5 4 3 2 1
Repeating an action a fixed number of times

If you need to repeat something a set number of times but don't actually care about the current value, the idiomatic pattern is for _ in range(n): — using an underscore as a throwaway variable name, as covered in the earlier article on naming conventions:

for _ in range(3):
    print("Hello!")
# Hello!
# Hello!
# Hello!

The underscore signals to anyone reading the code: "this loop variable is intentionally unused, only the repetition count matters here."

Getting the Index with enumerate()

Why for i in range(len(list)) is unpythonic

A very common pattern beginners reach for — especially coming from languages that require manual indexing — looks like this:

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

# Works, but not the Python way
for i in range(len(fruits)):
    print(i, fruits[i])

This technically works, but it's considered unpythonic: it reintroduces manual index management, which is exactly the kind of bookkeeping Python's for loop was designed to eliminate in the first place. It's also more error-prone and less readable than the alternative below.

The cleaner alternative: enumerate()

Python's built-in enumerate() function gives you both the index and the value together, in a single, readable loop:

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

for index, fruit in enumerate(fruits):
    print(index, fruit)
# 0 apple
# 1 banana
# 2 cherry

This is the idiomatic way to get an index while looping in Python — it reads naturally and doesn't require you to separately index back into the list on every iteration.

The start parameter

If you need counting to begin from something other than 0 — a common need for user-facing output, where people generally expect 1-based numbering — enumerate() accepts a start parameter:

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

for position, fruit in enumerate(fruits, start=1):
    print(f"{position}. {fruit}")
# 1. apple
# 2. banana
# 3. cherry

Nested for Loops and Loop Control

Nesting one for loop inside another

Just like conditionals, for loops can be nested — useful when you need to consider every combination of items from two (or more) sequences:

colors = ["red", "blue"]
sizes = ["S", "M"]

for color in colors:
    for size in sizes:
        print(f"{color} - {size}")
# red - S
# red - M
# blue - S
# blue - M

For every single item in the outer loop, the entire inner loop runs completely before moving on to the next outer item — which is why this produces every possible combination.

A cleaner alternative: itertools.product()

For generating combinations specifically, Python's itertools module offers product(), which does the same thing as nested loops but often reads more clearly once you're comfortable with it:

from itertools import product

colors = ["red", "blue"]
sizes = ["S", "M"]

for color, size in product(colors, sizes):
    print(f"{color} - {size}")

This produces identical output to the nested loop above, but flattens it into a single loop — worth knowing about once nested loops start feeling repetitive, though the plain nested version is perfectly fine and often clearer for simple two-level cases.

break and continue

Two keywords give you finer control over a loop's execution:

# break — exits the loop immediately, skipping any remaining iterations
for number in range(10):
    if number == 5:
        break
    print(number)
# 0 1 2 3 4

# continue — skips the rest of the current iteration, moves to the next one
for number in range(5):
    if number == 2:
        continue
    print(number)
# 0 1 3 4
The lesser-known for...else construct

Python has a genuinely unusual feature that surprises even some experienced developers: a for loop can have an else clause, which runs only if the loop completes normally — meaning it finishes all its iterations without hitting a break.

numbers = [1, 3, 5, 7, 9]

for number in numbers:
    if number % 2 == 0:
        print("Found an even number")
        break
else:
    print("No even numbers found")
# No even numbers found — the loop ran to completion, so the else block executed

This is a niche but genuinely useful pattern for search-style loops: "look for something; if a break finds it, do one thing; if the loop finishes without ever breaking, do another." It's not commonly used, but recognizing it will save you confusion the first time you encounter it in someone else's code.