Last week, I had to implement something in Python and found myself conflicted between for loop vs. list comprehension approaches. Both approaches seemed equally readable to me. I was especially surprised by how powerful list comprehensions are and deeply appreciated this as a language feature.
Technically, I ended up using a set comprehension rather than a list comprehension because I wanted to avoid duplicate conflict pairs. So let’s go through what the implementation was.
The Setup
The problem was essentially to create a conflict checker and I ended up with this particular implementation:
from dataclasses import dataclass
@dataclass
class Person:
conflicts_with: set[str]
class ConflictChecker:
def __init__(self):
self.people: dict[str, Person] = {}
def add_person(self, person_id: str, conflicts_with: set[str]):
"""
Registers a person and others they have conflicts with.
"""
if person_id in self.people:
self.people[person_id].conflicts_with.update(conflicts_with)
else:
self.people[person_id] = Person(conflicts_with)
def check_conflicts(self, attendees: list[str]) -> list[tuple[str, str]]:
"""
Given a list of attendees, returns any pairs who have known conflicts.
"""
conflicts = set()
for person_id in attendees:
person = self.people.get(person_id)
if person is None:
continue
for other in person.conflicts_with:
if other not in attendees:
continue
conflicts.add(tuple(sorted((person_id, other))))
return list(conflicts)
Its usage would look like:
conflict_checker = ConflictChecker()
conflict_checker.add_person(
person_id="Alice",
conflicts_with={"Karen", "Jack"}, # past team tension
)
conflict_checker.add_person(
person_id="Bob",
conflicts_with={"Grace", "Ivy"}, # clashed during a product launch
)
conflict_checker.add_person(
person_id="Charlie",
conflicts_with={"Freddie"}, # personality mismatch
)
conflict_checker.add_person(
person_id="Eve",
conflicts_with={"Alice", "Jack"}, # disagreement over hiring decisions
)
conflict_checker.add_person(
person_id="Karen",
conflicts_with={"Bob", "Grace"}, # often disagrees in meetings
)
attendees = ["Alice", "Bob", "Charlie", "Eve", "Grace", "Jack"]
conflicts = conflict_checker.check_conflicts(attendees)
print(conflicts)
Finally as the output of the above, here are the (unordered) pairs of people that would not get along:
# Output:
[('Eve', 'Jack'), ('Alice', 'Eve'), ('Bob', 'Grace'), ('Alice', 'Jack')]
The Problem
After writing for loops, I have a habit of rewriting them into list
comprehensions to see if they read better. A rewrite of check_conflicts
would
look like:
def check_conflicts(self, attendees: list[str]) -> list[tuple[str, str]]:
return list(
{
tuple(sorted((person_id, other)))
for person_id in attendees
if (person := self.people.get(person_id)) is not None
for other in person.conflicts_with
if other in attendees
}
)
For me, this reads rather well because:
- what’s returned from the method is more explicitly expressed up front
- it’s more concise, reducing the cognitive load
- it avoids deep nesting, which reduces complexity
But the for-loop approach is more language-agnostic, and thus extremely
familiar. Personally, the main downside of the set comprehension is debugging.
It’s harder to decide where to break with pdb.set_trace()
.
In the end, I went with the set comprehension, not just for performance, but because I found it more expressive and readable. That said, I wouldn’t hesitate to fall back on a classic loop for anything more complex or debug-heavy.
When deciding between loops and comprehensions, I often ask: Is the logic linear and readable at a glance? If yes, comprehension wins. If it needs explaining, stick to the loop.