AI SWE Prep
0/9in Python Foundations
Python Foundations

Idioms, Comprehensions & Iterators

Comprehensions, unpacking, and the iterator protocol. Write Python that reads like Python instead of translated Java.

~14 minLesson 5 of 60
Your progressNot started

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

Building a sequence: three stylestap a column to focus
for + appendcomprehensiongenerator
Readabilityverboseconciseconcise
Memorywhole listwhole listone item
Reusable resultyesyesone-shot
Best whenside effects / complexyou need the full liststreaming / large data
Prefer comprehensions for clarity; switch to generators when data is large or infinite.

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.

ExerciseEasy
  • Comprehension
  • Iteration

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.

ExerciseMedium
  • Readability
  • Comprehension

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.”

GotchaMedium
  • enumerate
  • zip
  • Unpacking

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.

Your progressNot started