skip to content

Search

Python List (In)Comprehension: Cleaner or Confusing?

3 min read

Clean Python code: compare list comprehensions and for loops with a practical example, and learn when each makes your code better or worse.

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.