Introduction
Welcome to a new module focused on writing fluent, idiomatic, and efficient loops. In the previous module, we concluded our deep dive into Python's data model, learning how to use dunder methods and descriptors to make your custom objects feel native to the language.
Now, we shift our focus from object design to a fundamental aspect of daily coding: iteration. Many programmers coming from languages like C++ or Java carry over looping patterns that are verbose and error-prone in Python.
This first lesson targets the most common of these "Java-isms." Your goal is to replace the range(len(...)) construct with direct iteration. Mastering this single change is one of the quickest ways to make your Python code more readable, safer, and more performant.
The Anti-Pattern: The C-Style Loop in Python
If you have a background in Java or C, you are accustomed to writing loops by managing an index variable, like this Java snippet:
// Java
for (int i = 0; i < myList.size(); i++) {
String item = myList.get(i);
System.out.println(item);
}
A direct, but un-Pythonic, translation of this logic into Python looks like this:
# Un-Pythonic ("Java-style")
my_list = ["apple", "banana", "cherry"]
for i in range(len(my_list)):
item = my_list[i]
print(item)
This range(len(...)) pattern is a classic anti-pattern in Python. It works, but it's needlessly complex and misses the point of Python's for loop, which is designed as a "for-each" construct.
This image neatly summarizes the difference in mindset:

To understand exactly why the range(len()) approach is considered an anti-pattern, let's analyze it more closely.
The article '"Range len", the anti-pattern' provides a methodical breakdown of why this pattern is suboptimal. It forces you to think about what you actually need versus the intermediate steps you're taking to get it.
Read the sections "Anti-pattern" and "Analysis". Focus on how the author deconstructs the for i in range(len(text)) loop to reveal its unnecessary complexity.
The Pythonic Solution: Direct Iteration
The correct, Pythonic way to iterate over the elements of a collection is to do so directly.
# Pythonic
my_list = ["apple", "banana", "cherry"]
for item in my_list:
print(item)
This code does the exact same thing as the previous example, but with significant advantages:
- Readability: The code reads like plain English: "For each item in my list, print the item." This clarity makes the code's intent immediately obvious.
- Simplicity: It eliminates the need for managing an index variable (
i), callinglen(), creating arangeobject, and performing a manual lookup (my_list[i]). - Safety: By removing manual indexing, you completely eliminate the risk of off-by-one errors and other common indexing bugs.
Now, continue with the article you were reading to see the analysis of this superior pattern.
Let's return to the article '"Range len", the anti-pattern' to see the analysis of the direct iteration approach.
Read the sections "What to do?", "How to do it?", and "Analysis of the new pattern". Notice how simple the explanation becomes when using the direct iteration pattern.
Under the Hood: Performance and the Iterator Protocol
You might think the difference is purely stylistic, but it's not. Direct iteration is generally faster because it leverages Python's highly optimized internal iterator protocol.
The range(len()) approach adds overhead:
- The
len()function is called. - A
rangeobject is created. - On each iteration, a subscription
my_list[i]is performed, which is an extra lookup.
Direct iteration, for item in my_list, directly asks the list for its iterator and then fetches the next item on each loop, involving fewer steps. Let's see the proof.
Python Indexing vs. For Loops: What's Really Faster?
The article 'Python Indexing vs. For Loops: What's Really Faster?' provides benchmarks and a clear explanation for the performance difference.
Read the sections from "Speed and Performance" to the end. Pay close attention to the benchmark results and the explanation in "Why Are for Loops Faster?". The summary and 'Next Steps' sections reinforce the main takeaways.
As the article mentions, this performance difference becomes more significant with larger datasets. While the primary motivation for writing Pythonic code is readability and maintainability, the performance benefit is a welcome bonus.
A Mandate from a Python Core Developer
This isn't just a community preference; it's a core principle of the language's design. Raymond Hettinger, a long-time Python core developer, is famous for his talks on writing beautiful, idiomatic Python. His advice on this topic is blunt and unequivocal.
Transforming Code into Beautiful, Idiomatic Python
In this classic clip from his talk 'Transforming Code into Beautiful, Idiomatic Python', Raymond Hettinger directly addresses the range(len()) anti-pattern.
Watch the segment from 04:43 to 05:32. Listen to his direct instruction to the audience: go back through your code and fix this pattern wherever you see it.
Hettinger's core message—"when you use an index in Python... you're almost always doing it wrong"—is a powerful guideline to internalize as you transition away from a Java-centric mindset.
Exercise: Refactor a Java-ism
Now it's time to put this into practice. Below is a function that processes a list of dictionaries. It's written using the range(len()) anti-pattern. Your task is to refactor it to use direct, Pythonic iteration.
Your Task: Rewrite the process_invoices function to eliminate the range(len()) loop.
# initial_code.py
def process_invoices(invoices):
"""
Processes a list of invoices and prints a summary for each overdue invoice.
"""
print("--- Overdue Invoice Report ---")
# --- REFACTOR THIS LOOP ---
for i in range(len(invoices)):
invoice = invoices[i]
if invoice["status"] == "overdue":
print(f" - Invoice #{invoice['id']}: ${invoice['amount']:.2f} is overdue.")
# -------------------------
print("----------------------------")
# Sample data
invoice_list = [
{"id": 101, "amount": 150.00, "status": "paid"},
{"id": 102, "amount": 25.50, "status": "overdue"},
{"id": 103, "amount": 500.00, "status": "paid"},
{"id": 104, "amount": 75.00, "status": "overdue"},
]
process_invoices(invoice_list)
Click to see the solution
# solution_code.py
def process_invoices(invoices):
"""
Processes a list of invoices and prints a summary for each overdue invoice.
"""
print("--- Overdue Invoice Report ---")
# Pythonic direct iteration
for invoice in invoices:
if invoice["status"] == "overdue":
print(f" - Invoice #{invoice['id']}: ${invoice['amount']:.2f} is overdue.")
print("----------------------------")
# Sample data
invoice_list = [
{"id": 101, "amount": 150.00, "status": "paid"},
{"id": 102, "amount": 25.50, "status": "overdue"},
{"id": 103, "amount": 500.00, "status": "paid"},
{"id": 104, "amount": 75.00, "status": "overdue"},
]
process_invoices(invoice_list)
# Expected output:
# --- Overdue Invoice Report ---
# - Invoice #102: $25.50 is overdue.
# - Invoice #104: $75.00 is overdue.
# ----------------------------
Notice how the refactored code is cleaner and more directly expresses the logic: "for each invoice in the list...".
Conclusion
In this lesson, you've taken a significant first step toward writing fluent Python loops. You've learned to identify and replace the range(len()) anti-pattern, a common habit for those coming from languages like Java.
Key Takeaways:
- The Anti-Pattern:
for i in range(len(iterable))is an un-Pythonic "Java-ism." It is verbose, less readable, and prone to indexing errors. - The Pythonic Way:
for item in iterableis the correct way to loop in Python. It's more readable, simpler, safer, and often more performant. - Performance: Direct iteration is faster because it uses Python's optimized iterator protocol, avoiding the overhead of index lookups on each iteration.
- Mindset Shift: Python's
forloop is a "for-each" loop. Embracing this is crucial for writing idiomatic code.
Of course, there are times when you genuinely need an index. But even then, there's a better way than range(len()).
In the next lesson, we will cover the Pythonic solution for this exact scenario: the built-in enumerate() function. We will also explore zip() for iterating over multiple collections in parallel.