Idioms, Comprehensions & Iterators
Comprehensions, unpacking, and the iterator protocol. Write Python that reads like Python instead of translated Java.
Coming from another language, you can write Python that works while still writing it with a foreign accent. Fluent Python leans on a small set of idioms and on the data model — the set of special methods that let your own objects behave like built-in ones. This lesson is about writing code that reads like Python.
Comprehensions over manual loops
The single most recognizable Python idiom. A comprehension builds a list, dict, or set in one expressive line:
squares = [n * n for n in range(10)]
evens = [n for n in nums if n % 2 == 0]
by_id = {user.id: user for user in users}
unique_words = {w.lower() for w in text.split()}
They’re not just shorter — they signal intent (“I am transforming/filtering a collection”) more clearly than an accumulator loop. Reach for a plain loop only when there are side effects or the logic gets complex enough to hurt readability.
Iterators and generators: laziness as a superpower
| for + append | comprehension | generator | |
|---|---|---|---|
| Readability | verbose | concise | concise |
| Memory | whole list | whole list | one item |
| Reusable result | yes | yes | one-shot |
| Best when | side effects / complex | you need the full list | streaming / large data |
An iterator produces values one at a time; a generator is the easiest way to make one, using yield. The payoff is that you can process data that never fully exists in memory at once — a million-line file, an infinite sequence, a paginated API.
def read_large_file(path):
with open(path) as f:
for line in f: # the file object is itself a lazy iterator
yield line.strip()
# processes one line at a time, constant memory
for line in read_large_file("huge.log"):
handle(line)
Generators are the backbone of memory-efficient Python and pair naturally with data pipelines — exactly the kind you’ll build when preprocessing text for an embedding model.
The data model: dunder methods
Python’s “magic” is not magic — it’s a set of dunder (double-underscore) methods the language calls on your behalf. Implement them and your objects plug into the language’s syntax:
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self): # what you see in a debugger
return f"Vector({self.x}, {self.y})"
def __add__(self, other): # enables a + b
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other): # enables a == b
return (self.x, self.y) == (other.x, other.y)
def __len__(self): # enables len(a)
return 2
len(obj), obj[i], a + b, for x in obj, with obj, print(obj) — each is just Python calling a dunder. Understanding this demystifies the whole language: built-in types and your own classes play by identical rules.
A handful of idioms that mark fluency
# Unpacking and swapping — no temp variable
a, b = b, a
first, *rest = [1, 2, 3, 4] # first=1, rest=[2,3,4]
# enumerate instead of range(len(...))
for i, item in enumerate(items): ...
# zip to walk collections in parallel
for name, score in zip(names, scores): ...
# Truthiness — empty collections are falsy
if not results: # not len(results) == 0
return
# Context managers guarantee cleanup
with open(path) as f: # file closes even on exception
data = f.read()
Why it’s worth the effort
Idiomatic code is faster to read, easier to review, and less bug-prone because it uses well-trodden paths. In an interview, fluent Python is a quiet signal that you’ve written a lot of it. In a codebase, it’s the difference between code your teammates skim and code they have to decode.
When to reach for this
Variations
Try it
These exercises are about making intent visible. Shorter is not the goal; removing bookkeeping noise is.
Rewrite a Java-ish loop
Rewrite this in idiomatic Python:
active_emails = []
for i in range(len(users)):
user = users[i]
if user["active"] == True:
active_emails.append(user["email"].lower())Approach. The loop only filters and transforms. Iterate over users directly, lean on truthiness, and make the output expression the first thing the reader sees.
Show solution
active_emails = [
user["email"].lower()
for user in users
if user["active"]
]There is no index logic left because the index was never part of the problem. If the body grew side effects, logging, or several branches, a plain loop would be more readable again.
Choose comprehension or loop
You need labels for scores: "pass" for 70 and above, "review" for 50–69, and "fail" below 50. Write the version you would put in a code review.
Approach. A nested conditional expression inside a comprehension is possible, but this rule has enough branches that a small named helper keeps the comprehension readable.
Show solution
def label_score(score: int) -> str:
if score >= 70:
return "pass"
if score >= 50:
return "review"
return "fail"
labels = [label_score(score) for score in scores]This keeps the collection shape obvious while giving the branching rule a name. The idiom is not “always one line”; it is “put each idea at the right level.”
Avoid enumerate and zip surprises
You pair names and scores, but one score is missing. What happens by default, and how do you make that mismatch fail loudly?
names = ["Ada", "Grace", "Katherine"]
scores = [98, 95]
for i, (name, score) in enumerate(zip(names, scores), start=1):
print(i, name, score)Approach. zip stops at the shortest input, so Katherine disappears. In Python 3.10+, strict=True turns that silent truncation into a ValueError.
Show solution
names = ["Ada", "Grace", "Katherine"]
scores = [98, 95]
for rank, (name, score) in enumerate(zip(names, scores, strict=True), start=1):
print(rank, name, score)The original loop prints only Ada and Grace. Use default zip when truncation is intended; use strict=True when parallel collections are supposed to have the same length.