How to Make Your Python Loops More Pythonic
Pythonize your C-style “for” and “while” loops by refactoring them using generators and other techniques.
One of the easiest ways to spot a developer with a background in C-style languages who only recently picked up Python is to look at how they write loops.
For example, whenever I see a code snippet like this, that’s an example of someone trying to write Python like it’s C or Java:
my_items=['a','b','c']i=0whilei<len(my_items):print(my_items[i])i+=1
Now, what’s so “unpythonic” about this code?
Two things could be improved in this code example:
- First, it keeps track of the index
i
manually—initializing it to zero and then carefully incrementing it upon every loop iteration. - And second, it uses
len()
to get the size of a container in order to determine how often to iterate.
In Python you can write loops that handle both of these responsibilities automatically. It’s a great idea to take advantage of that.
If your code doesn’t have to keep track of a running index it’s much harder to write accidental infinite loops, for example. It also makes the code more concise and therefore more readable.
How to track the loop index automatically
To refactor the while
-loop in the code example, I’ll start by removing the code that manually updates the index. A good way to do that is with a for
-loop in Python.
Using Python’s range()
built-in I can generate the indexes automatically (without having to increment a running counter variable):
>>>range(len(my_items))range(0,3)>>>list(range(0,3))[0,1,2]
The range
type represents an immutable sequence of numbers. It’s advantage over a regular list
is that it always takes the same small amount of memory.
Range objects don’t actually store the individual values representing the number sequence—instead they function as iterators and calculate the sequence values on the fly.
(This is true for Python 3. In Python 2 you’ll need to use the xrange()
built-in to get this memory-saving behavior, as range()
will construct a list object containing all the values.)
Now, instead of incrementing i
on each loop iteration, I could write a refactored version of that loop like this:
foriinrange(len(my_items)):print(my_items[i])
This is better. However it still isn’t super Pythonic—in most cases when you see code that uses range(len(...))
to iterate over a container it can be improved and simplified even further.
Let’s have a look at how you might do that in practice.
💡 Python’s “for” loops are “for-each” loops
As I mentioned, for
-loops in Python are really “for-each” loops that can iterate over items from a container or sequence directly, without having to look them up by index. I can take advantage of that and simplify my loop even further:
foriteminmy_items:print(item)
I would consider this solution to be quite Pythonic. It’s nice and clean and almost reads like pseudo code from a text book. I don’t have to keep track of the container’s size or a running index to access elements.
The container itself takes care of handing out the elements so they can be processed. If the container is ordered, so will be the resulting sequence of elements. If the container is not ordered it will return its elements in an arbitrary order but the loop will still cover all of them.
What if I need the item index?
Now, of course you won’t always be able to rewrite your loops like that. What if you need the item index, for example? There’s a Pythonic way to keep a running index that avoids the range(len(...))
construct I recommended against.
The enumerate()
built-in is helpful in this case:
>>>fori,iteminenumerate(my_items):...print(f'{i}: {item}')0:a1:b2:c
You see, iterators in Python can return more than just one value. They can return tuples with an arbitrary number of values that can then be unpacked right inside the for
-statement.
This is very powerful. For example, you can use the same technique to iterate over the keys and values of a dictionary at the same time:
>>>emails={...'Bob':'bob@example.com',...'Alice':'alice@example.com',...}>>>forname,emailinemails.items():...print(f'{name} → {email}')'Bob → bob@example.com''Alice → alice@example.com'
Okay, what if I just have to write a C-style loop?
There’s one more example I’d like to show you. What if you absolutely, positively need to write a C-style loop. For example, what if you must control the step size for the index? Imagine you had the following original C or Java for
-loop:
for(inti=a;i<n;i+=s){// ...}
How would this pattern translate to Python?
The range()
function comes to our rescue again—it can accept extra parameters to control the start value for the loop (a
), the stop value (n
), and the step size (s
).
Therefore our example C-style loop could be implemented as follows:
foriinrange(a,n,s):# ...
Main Takeaways
- Writing C-style loops in Python is considered unpythonic. Avoid managing loop indexes and stop conditions manually if possible.
- Python’s
for
-loops are really “for-each” loops that can iterate over items from a container or sequence directly.
📺 Watch a video tutorial based on this article
» Subscribe to the dbader.org YouTube Channel for more videos.