Learn how for loops work in Python, from the basics of iterating over lists and dictionaries to advanced patterns using enumerate(), zip(), chain(), and more.
In Python, if you want to repeat an action for each item in a collection—without writing the same line of code over and over—a for loop is your tool of choice.
It’s straightforward, readable, and designed to let you focus on what you’re doing with each item, not how you’re moving through them. Compared to other languages that rely on manual index management, Python’s for
loop feels refreshingly high-level.
Here’s the basic form:
for item in iterable:
# do something with item
That’s it. No counters, no setup, no post-loop cleanup. Python handles the plumbing so you can focus on the logic.
By the way, here's the video version of this article. Perfect if you want a short, high level overview before diving into this more in depth article.
Python’s for
loop is built for clarity and expressiveness. Some common use cases include:
If it’s iterable, it’s loopable. And in Python, most things are iterable—by design.
Here’s a basic loop over a list:
names = ['Alice', 'Bob', 'Charlie']
for name in names:
print(f"Hello, {name}")
# Hello, Alice
# Hello, Bob
# Hello, Charlie
It’s clear what’s happening. No index juggling. Just a loop that gives you each item in turn.
When we say for item in iterable, we mean any object that can return its elements one at a time. That includes lists, tuples, strings, sets, dictionaries, files, and more.
for letter in "Python":
print(letter)
This will print each character in the string. Behind the scenes, Python is requesting each element from the string’s iterator. But you don’t need to manage that yourself—unless you're doing something more advanced, like writing custom iterable classes.
For now, it’s enough to know that if something “feels” like a sequence, chances are Python lets you loop over it.
For a detailed dive into iterables, see Python Iterators vs Iterables.
Python’s for loop works with any iterable—and some of the most common ones are the built-in collection types. Lists, tuples, strings, dictionaries, and sets all support straightforward, readable iteration.
Let’s walk through the essentials.
Lists and tuples are ordered sequences. Looping over them is exactly what you’d expect:
items = ['apple', 'banana', 'cherry']
for item in items:
print(item)
# apple
# banana
# cherry
Python hands you each element in order—left to right, no surprises. Tuples work the same way:
coordinates = (4, 7, 9)
for value in coordinates:
print(value)
The only real difference between lists and tuples in this context is that tuples are immutable. But as far as looping goes, they're interchangeable.
Strings are sequences of characters, which makes them iterable too:
for char in "loop":
print(char)
# l
# o
# o
# p
This can be handy for parsing or transforming text one character at a time. If you’ve ever needed to count vowels, reverse a word, or inspect input, you’ve likely done something like this.
Sets are unordered collections of unique items. You can loop over them, but keep in mind that the order of iteration is not guaranteed:
colors = {'red', 'green', 'blue'}
for color in colors:
print(color)
# green
# blue
# red
Or possibly a different order—don’t use sets if the order matters.
Dictionaries require a bit more care, since each item is a key–value pair. You can loop through the keys:
person = {'name': 'Alice', 'age': 30}
for key in person:
print(key)
But most of the time, you’ll want to access both the key and its value:
for key, value in person.items():
print(f"{key}: {value}")
If you only need values:
for value in person.values():
print(value)
Or just the keys (explicitly):
for key in person.keys():
print(key)
Dictionaries offer flexibility, but make sure you're clear about what you’re looping over—keys, values, or both.
Iterable Type | Yields | Notes |
---|---|---|
List / Tuple | Each item | Ordered, common use case |
String | Each character | Also ordered |
Set | Each item | Unordered, items are unique |
Dictionary | Keys by default | Use .items() or .values() as needed |
Python makes looping over collections feel natural. In most cases, it’s as simple as writing for x in y
. Just be aware of what your iterable is giving you—and how you want to use it.
When you want to loop a specific number of times—or you need to generate a sequence of numbers—the built-in range()
function is your friend. It’s efficient, predictable, and plays nicely with for loops.
You can think of range()
as a generator of numbers: it doesn’t produce a list outright, but instead gives you each number when the loop asks for it.
At its simplest, range()
takes one argument—the stop value—and starts counting from zero:
for i in range(5):
print(i)
# 0
# 1
# 2
# 3
# 4
Note: the stop value is exclusive, so this prints up to 4, not 5. A small detail, but a common gotcha when you’re just starting out.
You can also provide a start value explicitly:
for i in range(2, 6):
print(i)
# 2
# 3
# 4
# 5
Still exclusive on the upper bound.
Want to skip numbers? Use the optional step argument:
for i in range(0, 10, 2):
print(i)
# 0
# 2
# 4
# 6
# 8
The third argument controls how much to increment each time. This is useful for even/odd iteration, sampling, or any case where you don’t want to visit every single number.
You can also step backward using a negative step:
for i in range(5, 0, -1):
print(i)
# 5
# 4
# 3
# 2
# 1
Yes, you can count down. No need to reverse a list or do anything fancy.
A common use of range()
is when you need access to both an index and an item:
items = ['a', 'b', 'c']
for i in range(len(items)):
print(i, items[i])
This works, but it's not the most Pythonic way—you’ll typically prefer enumerate()
for that, which we’ll cover shortly. Still, this pattern is useful when you're dealing with slices, skipping elements, or need more control over the loop.
In Python 3, range()
returns a special range object that generates values as needed. It doesn’t create a list in memory (unless you explicitly convert it), which makes it efficient even for large numbers:
# Safe and efficient
for i in range(1_000_000):
pass
This is why you don’t need to worry about range()
being slow or memory-hungry—unless you force it into a list.
range(stop)
→ starts from 0range(start, stop)
→ counts from start
to stop - 1
range(start, stop, step)
→ adds a step (positive or negative)for
loops, especially when you need numeric iterationIt’s one of those Python tools that does one thing, does it well, and quietly handles edge cases you didn’t even know were edge cases.
Sometimes, when you're looping through a sequence, you need to know not just what you're iterating over, but where you are in that sequence. Sure, you could reach for range(len(...))
, but Python has a cleaner, more expressive option: enumerate()
.
It’s built for those situations where the index actually matters—but you’d rather not clutter your code with indexing syntax.
Let’s take a quick look:
items = ['a', 'b', 'c']
for i in range(len(items)):
print(i, items[i])
It works, but it feels a bit low-level. You’re manually juggling indices and elements, and it’s easy to trip over an off-by-one error or an accidental mismatch.
Python’s enumerate()
function solves this neatly by giving you the index and the item—together:
for index, item in enumerate(items):
print(index, item)
# 0 a
# 1 b
# 2 c
Clean. Readable. No need to poke into the list using square brackets. Just ask Python to hand you both things you need.
By default, enumerate()
starts counting from zero. But if you want the index to start somewhere else—say, 1—you can pass a second argument:
for index, item in enumerate(items, start=1):
print(index, item)
# 1 a
# 2 b
# 3 c
This is useful when displaying numbered lists to users, or if your data is 1-based (as spreadsheets and humans tend to be).
Use it whenever:
range()
and len()
for iterationIt’s a small change, but it makes your code feel more intentional—and in larger codebases, clarity is more than just nice to have.
enumerate()
simplifies loops where index mattersrange(len(...))
It’s one of those features that feels minor—until you go back to writing loops without it.
Python’s for loop goes through every item unless you tell it otherwise. That’s where break
and continue
come in. These two control statements let you adjust the flow mid-loop, giving you the option to stop early or skip selectively.
They’re simple tools, but when used well, they make your loops more flexible and expressive.
The break
statement immediately exits the loop—no further iterations, no questions asked. It’s most commonly used when you’re searching for something and want to stop once you’ve found it.
numbers = [3, 7, 12, 18, 21]
for number in numbers:
if number > 10:
print("Found a number greater than 10:", number)
break
# Found a number greater than 10: 12
After 12 is found, the loop ends—18 and 21 never even get a chance.
Just make sure it’s obvious why the loop is ending early. A well-placed comment doesn’t hurt.
The continue statement on the other hand skips the current iteration and moves to the next one.
for number in range(5):
if number == 2:
continue
print(number)
# 0
# 1
# 3
# 4
Here, 2 is simply skipped. The loop goes on as if it never existed—useful when filtering or ignoring certain values.
None
values
Bypassing certain branches of logicJust be cautious—overusing continue
can lead to loops that are harder to read. If your logic starts looking like a choose-your-own-adventure novel, it might be time to rethink the structure.
break
and continue
can also appear in the same loop—just be clear about their purpose:
for number in range(10):
if number == 0:
continue # skip zero to avoid division error
if number > 5:
break # only process numbers 1–5
print(10 / number)
A bit artificial, but it illustrates the point: skip what doesn’t make sense, and exit when you’ve had enough.
Statement | What It Does | Typical Use Case |
---|---|---|
break |
Exits the loop immediately | Stop once a condition is met |
continue |
Skips to the next iteration | Skip invalid or unwanted cases |
Used well, these control tools make your loops precise and intention-driven. Just don’t overdo it—sometimes a cleaner loop structure is better than a clever one.
Sometimes you’re working with two (or more) related sequences—names and scores, questions and answers, keys and values stored in parallel lists. Iterating over them side by side is a common task, and Python’s zip()
function makes that both elegant and reliable.
Think of zip()
as a way to "pair up" the elements of multiple iterables, forming tuples of matching elements along the way.
Let’s say you’ve got two lists:
names = ['Alice', 'Bob', 'Charlie']
scores = [85, 92, 78]
To print each name with the corresponding score, you can write:
for name, score in zip(names, scores):
print(f"{name}: {score}")
# Alice: 85
# Bob: 92
# Charlie: 78
Python combines the first elements of both lists, then the second, and so on—like a zipper closing up two sides.
If the iterables are of different lengths, zip()
stops at the shortest one:
names = ['Alice', 'Bob']
scores = [85, 92, 78]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# Alice: 85
# Bob: 92
No errors, no warnings—Python simply stops where it makes sense. If you need to handle the leftover items, consider using itertools.zip_longest()
instead.
zip()
comes in handy when:
keys = ['name', 'age']
values = ['Alice', 30]
person = dict(zip(keys, values))
This is one of the most Pythonic ways to merge keys and values into a dictionary—brief, clear, and readable.
You can also reverse the process. If you’ve zipped a list of pairs, you can unzip it using the *
operator:
pairs = [('a', 1), ('b', 2), ('c', 3)]
letters, numbers = zip(*pairs)
Now letters
becomes ('a', 'b', 'c')
and numbers
becomes (1, 2, 3)
. It's a neat trick that's good to keep in your back pocket.
zip()
lets you iterate over multiple iterables in parallelitertools.zip_longest()
In short, if you’ve ever written for i in range(len(...))
just to access two lists side by side, zip()
is probably what you actually wanted.
Sometimes data doesn’t come in one tidy list—it comes in layers. A list of lists, for example, is a common structure when you're dealing with grouped or hierarchical data. And while nested loops can handle this just fine, Python offers a more streamlined option when you want to treat all the inner elements as one continuous sequence.
That’s where itertools.chain()
comes in.
Let’s say you have this:
groups = [
[1, 2],
[3, 4],
[5, 6]
]
You could loop over it using nested loops:
for group in groups:
for number in group:
print(number)
That works. But if you’re not interested in the grouping structure and just want all the numbers in one flat sequence, chain()
gives you a cleaner path.
itertools.chain()
flattens multiple iterables into a single sequence:
from itertools import chain
for number in chain([1, 2], [3, 4], [5, 6]):
print(number)
Or, using our earlier groups
list:
for number in chain.from_iterable(groups):
print(number)
Output:
1
2
3
4
5
6
No nested loops. Just one pass over the data, as if it were one big list to begin with.
If you're pulling data from multiple sources and want to loop through all of it without worrying about the container structure, chain()
is often the most elegant tool for the job.
For more on how generators can simplify such operations, see Generators in Python.
chain()
isn’t just for flattening. You can also use it to combine multiple lists:
a = [1, 2]
b = [3, 4]
c = [5, 6]
for number in chain(a, b, c):
print(number)
This gives the same result as [1, 2] + [3, 4] + [5, 6]
, but without creating a new combined list in memory first. That can make a difference if the iterables are large or streaming.
itertools.chain()
flattens or combines iterables into one seamless sequence.from_iterable()
when working with a list of listsWhile nested loops certainly have their place, chain()
is often the better tool when you don’t need to treat inner lists differently—it helps your code express intent more clearly, and that’s never a bad thing.
Python gives you a few simple, flexible tools to loop over data in a specific order—whether that means sorting it, reversing it, or both. Importantly, these operations don’t require you to modify the original data (unless you explicitly want to), which makes your code safer and more predictable.
Let’s look at two common built-in functions: sorted()
and reversed()
.
The sorted()
function returns a new sorted list from any iterable. It leaves the original data untouched, which makes it a safe choice when you need to sort temporarily—for display, comparison, or output.
numbers = [5, 2, 9, 1]
for num in sorted(numbers):
print(num)
# 1
# 2
# 5
# 9
Notice: numbers
stays exactly as it was. If you want to sort in-place, you’d use .sort()
—but for most loop scenarios, sorted() is safer and more flexible.
Just add the reverse=True
flag:
for num in sorted(numbers, reverse=True):
print(num)
# 9
# 5
# 2
# 1
You can also sort using a custom function:
words = ['banana', 'apple', 'cherry']
for word in sorted(words, key=len):
print(word)
# apple
# banana
# cherry
Here, the words are sorted by length. You’re not limited to len()
—any function that returns a sortable value can be used.
This is especially useful when working with objects or complex data:
people = [{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}]
for person in sorted(people, key=lambda p: p['age']):
print(person['name'])
# Bob
# Alice
The reversed()
function gives you the same items, but in reverse order:
letters = ['a', 'b', 'c']
for letter in reversed(letters):
print(letter)
# c
# b
# a
Just like sorted()
, this doesn’t modify the original data. It simply gives you a view of it moving backward.
Note: reversed()
only works with sequences (like lists or tuples)—not arbitrary iterables. If you need to reverse something like a generator, convert it to a list first.
Function | Purpose | Mutates Original? |
---|---|---|
sorted() |
Returns a sorted version | No |
.sort() |
Sorts in place (lists only) | Yes |
reversed() |
Returns a reverse view | No |
Whether you're sorting numbers, reversing strings, or ordering complex data by a custom rule, these functions let you control the flow of your loops without extra steps or temporary variables.
There’s a moment in every Python developer’s journey when they discover comprehensions—and wonder how they ever lived without them.
Comprehensions are a compact, expressive way to generate new sequences from existing ones. They often replace short for
loops with a single, readable line. But while they can improve clarity and performance, they’re not always the right tool.
For a detailed dive into comprehensions, see Python Comprehensions.
A list comprehension is a concise way to create a new list by transforming or filtering items from an iterable.
Here’s the classic example:
squares = [x**2 for x in range(5)]
Equivalent for loop:
squares = []
for x in range(5):
squares.append(x**2)
Both do the same thing, but the comprehension gets to the point faster. If the transformation is simple and side-effect-free, comprehensions tend to be the more Pythonic choice.
You can also add an if clause to filter items:
evens = [x for x in range(10) if x % 2 == 0]
This gives you just the even numbers. Clean, readable, and no need to manually check inside the loop.
The same syntax works with other collection types:
Set comprehension:
unique_lengths = {len(word) for word in ['hi', 'hello', 'world']}
Dictionary comprehension:
name_lengths = {name: len(name) for name in ['Alice', 'Bob', 'Charlie']}
These are just as expressive and tend to communicate intent well, especially for small, self-contained operations.
Not every loop should be compressed into one line. Sometimes the logic is too involved, or you’re doing more than just building a list.
Avoid comprehensions when:
For example:
# Not ideal
[print(x) for x in range(5)]
This works, but it’s not what comprehensions are for. If your goal isn’t to produce a new list, a for loop is the clearer, more honest approach.
Use for loops when... |
Use comprehensions when... |
---|---|
Logic spans multiple lines or steps | You’re transforming or filtering items cleanly |
You’re performing actions (e.g., printing) | You’re building a new collection |
Readability matters more than brevity | You want concise, expressive transformation logic |
Comprehensions are great—powerful, concise, and elegant. But like any power tool, they’re best used with intention. When in doubt, write it long-form first. If it reads cleanly as a comprehension, refactor. If not, leave it be.
Loops are powerful, but with great power comes the occasional head-scratcher. Some operations are perfectly safe inside a loop—others can quietly cause bugs or performance problems if you’re not careful.
Let’s look at what’s safe to do inside a for loop in Python… and what might deserve a second thought.
Most everyday tasks inside a loop are perfectly fine. You’re free to:
Examples:
numbers = [1, 2, 3]
for n in numbers:
print(n * 2) # Safe: simple operation
Or building a transformed list:
results = []
for n in numbers:
results.append(n * 2) # Safe: appending elsewhere
No surprises here. You’re working with the data, not against it.
Here’s where things get trickier.
Modifying the iterable you’re currently looping over—especially a list or dictionary—can lead to confusing behavior or silent bugs. This is especially common when removing or inserting items.
Problem Example:
numbers = [1, 2, 3, 4]
for n in numbers:
if n % 2 == 0:
numbers.remove(n) # Risky
You might expect [1, 3]
at the end. But you’ll likely end up with [1, 3, 4]
—because removing an item shifts the list, and the loop moves on without realizing it skipped something.
Safer Alternative: Loop Over a Copy
for n in numbers[:]: # Shallow copy
if n % 2 == 0:
numbers.remove(n) # Safe now
Or, better yet, use list comprehension to build a new list:
numbers = [n for n in numbers if n % 2 != 0]
Modifying a dictionary while looping over it (adding or deleting keys) raises a RuntimeError
in most cases. If you need to change a dictionary mid-loop, loop over a copy of the keys:
for key in list(my_dict.keys()):
if should_remove(key):
del my_dict[key]
Appending to the list you’re looping over? Technically legal, but often a sign that the code may spiral.
items = [1, 2, 3]
for item in items:
items.append(item * 2) # This never ends well
This will likely create an infinite loop or at least unexpected results. It’s best to separate reading from writing when working with collections in a loop.
Action | Safe? | Notes |
---|---|---|
Reading items | ✅ | Standard use case |
Appending to a separate collection | ✅ | Common in transformations |
Modifying the iterable in-place | ❌ | Can cause skipped items or runtime errors |
Looping over a copy | ✅ | Use [:] for lists or list(dict.keys()) for dicts |
Appending to the iterable being looped | ⚠️ | May lead to infinite or unintended behavior |
In short: if you're making structural changes to the iterable mid-loop, it's worth pausing to ask whether there's a better (and safer) approach. Python won't always stop you—but future you might wish it had.
Python’s for
loop is built for clarity and ease of use, but that doesn’t mean all loops are created equal. A well-structured loop can be efficient and readable. A poorly designed one… can quietly eat your CPU or leave future readers scratching their heads.
Here are a few principles and tips to help you write better loops—both in terms of performance and maintainability.
If you don’t need the index, don’t use it. Loop directly over the iterable:
# Better
for item in items:
...
# Avoid unless you need the index
for i in range(len(items)):
item = items[i]
Fewer moving parts usually means fewer bugs.
Avoid juggling indices manually when Python can give them to you cleanly:
for index, item in enumerate(items):
...
It’s more readable and reduces the chance of mismatched access.
Move expensive operations out of the loop body when possible.
# Inefficient
for item in data:
if lookup_value in expensive_function(): # runs every time
...
# Better
result = expensive_function()
for item in data:
if lookup_value in result:
...
Each call to a function, database, or file system inside a loop is a potential bottleneck.
As discussed earlier, changing the iterable while iterating over it (especially lists and dicts) can lead to unintended behavior. If modification is necessary, loop over a copy or build a new structure instead.
For quick filtering or transformation tasks, comprehensions offer both performance and readability benefits—when used in moderation.
# Good
squares = [x**2 for x in range(10)]
# Maybe not
squares = [x**2 for x in range(10) if x % 2 == 0 and x > 4 and some_other_check(x)]
If a comprehension feels like a riddle, it’s time to break it back into a loop.
Often, Python provides a built-in function that does exactly what your loop is doing—only faster and more idiomatically.
Examples:
sum()
instead of a loop that adds numbersany()
or all()
instead of custom flag logicmap()
or filter()
for straightforward transformations# Instead of:
total = 0
for x in numbers:
total += x
# Use:
total = sum(numbers)
When working with large datasets, prefer generators over building large lists:
# Generator expression
squares = (x**2 for x in range(10**6))
# Loop through it efficiently
for sq in squares:
process(sq)
This avoids loading the entire result into memory at once.
Tip | Why It Matters |
---|---|
Loop directly over data | Cleaner and easier to read |
Use enumerate() wisely |
Avoid manual index handling |
Don’t repeat expensive operations | Keeps loops fast |
Avoid mutating the iterable | Prevents subtle bugs |
Use comprehensions when appropriate | Keeps simple transformations compact |
Favor built-ins | More idiomatic and often faster |
Use generators for large data | Saves memory and improves scalability |
A well-written for
loop should be boring—in the best way. It should do one thing clearly, without surprises. With these practices in hand, you’ll not only write better loops, but you’ll make your code easier to debug, extend, and share.
Python’s for
loop is one of those features that feels effortless—until you realize how much thought and power it actually hides. From looping over simple lists to combining complex data structures with zip()
, flattening nested lists with chain()
, or customizing behavior with enumerate()
, Python makes iteration expressive without being verbose.
Along the way, we’ve covered:
What stands out across all of this is that Python encourages intention in your loops. Whether you’re looping over a few items or streaming through millions, the tools are there to help you write logic that’s both precise and expressive.
And perhaps that’s the real lesson: in Python, a for
loop isn’t just about repetition—it’s about communicating purpose.