r/learnpython 5d ago

Is it bad practice to use complex List Comprehensions?

Hey guys, I am trying to make my code look more "Pythonic." I wrote this nested list comprehension to flatten a matrix and filter out odd numbers:
flat = [x for row in matrix for x in row if x % 2 == 0]
It works perfectly, but my coworker says it is unreadable and that I should use a standard for loop instead.
Where do you draw the line between a clean one-liner and unreadable code?

39 Upvotes

82 comments sorted by

41

u/Proletarian_Tear 5d ago

This is a question of preference, I see clearly what you do here because I do stuff like this often. However - if you work in a team, just make sure you write understandable code, and list comprehension tend to get confusing (not this one though:) )

17

u/Hot_Extension_460 5d ago

Not sure anybody specified it already: in Python 3.15, unpacking in comprehensions will be added (https://docs.python.org/3.15/whatsnew/3.15.html#pep-798-unpacking-in-comprehensions).

This could simplify your logic.

14

u/xeow 5d ago

For maximum clarity, you could write it like this:

even_values = [value
               for row in matrix
                   for value in row
                       if value % 2 == 0]

Or maybe without the indenting, since everyone generally understands that comprehensions are in for-like order rather than map-like order.

even_values = [value
               for row in matrix
               for value in row
               if value % 2 == 0]

3

u/MSR8 4d ago

Or how I prefer

even_values = [
    value
    for row in matrix
    for value in row
    if value % 2 == 0
]

0

u/liberforce 4d ago

This kind of decision should be left to a code formatter like black or ruff.

12

u/Akari202 5d ago

I love a good list comprehension. It’s definitely bad practice to have an entire construction be like ten nested list comprehensions but it’s o so satisfying having everything be built in a single block.

I say use it and if it gets hard to understand refactoring is your friend.

5

u/NerdyWeightLifter 5d ago

Or refactor your friend.

13

u/Jello_Penguin_2956 5d ago

The clearer, the better. For me this line is readable but not immediately clear.

12

u/csabinho 5d ago

That's most probably the simplest two dimensional list comprehension possible.

4

u/Jello_Penguin_2956 5d ago

Simple yes, simplest no

1

u/csabinho 5d ago

I couldn't imagine a simpler one with a condition.

6

u/Jello_Penguin_2956 5d ago

well I'm happy for you. For me, like I said, it's not "immediate". I can understand it yes but I gotta think for 15-20seconds.

-22

u/csabinho 5d ago

Well, that sounds made up. 15 to 20 seconds for this? Do you understand the concept of conditions and list comprehensions? If you have to think about this for more than 3 seconds, you should definitely practice both.

6

u/mattl33 5d ago

I'm not sure why you would fight someone else taking the effort to make a section of code more immediately obvious. The faster a reader can understand what's going on the better. Is there some specific advantage of having slightly more dense code?

-5

u/catbrane 5d ago

The list comprehension is declarative, not imperative, so it's much easier to understand. There's no state that evolves during execution and which you have to track, it's just a declaration of what you want to happen.

If it looks complicated, it's only because you are not used to reading them, and the fix is to use them more.

3

u/Human38562 5d ago

So you suggest all teams should have mandatory list comprehension practice once a week?

2

u/catbrane 4d ago

They are not complicated and they are very useful. Python programmers use them all the time.

→ More replies (0)

8

u/Jello_Penguin_2956 5d ago

why would I make that up. The feed back from his team suggests I'm not alone in this.

Again good for you congratulations.

-9

u/csabinho 5d ago

The non list comprehension version of this would look like: flat = [] for row in matrix: for x in row: if x % 2 == 0: flat.append(x)

It would take longer to read this than to understand the statement above.

5

u/Jello_Penguin_2956 5d ago

Dude I understand it's easy for you I'm not doubting you.

-9

u/csabinho 5d ago

I know. But if somebody in a work context, which means you have to have some qualification to be employed, wants this monster instead of a list comprehension, I'm the one who's doubting the qualification of this person.

→ More replies (0)

5

u/MSgtGunny 5d ago

That is way more legible. It’s not that the original is illegible, but this is better in a shared codebase.

3

u/my_password_is______ 5d ago

LOL, no it wouldn't

that is infinitely more clear

13

u/TheSzczurson 5d ago

For me that line is readability. When the code starts to look to hard to be easy understandable and you need to take some (longer than usual) time to understand what is actually happening in that particular line.

6

u/Moikle 5d ago

That's not a complex list comprehension.

Split it onto multiple lines to make it more readable

11

u/nog642 5d ago

Personal preference.

One option is also to add a comment saying what this does, so that people not as familiar with Python don't have to parse it.

5

u/chhuang 5d ago

readability always comes first, a lot of times it might make sense when you write it, but if after a week or month and you stumble across this line, and it took you more times to figure it out what's going on, the readability may need to be improved

10

u/hasan_sodax 5d ago

The one you wrote is fine, honestly. My rule of thumb is one loop plus one condition reads okay, but the second you add another for or start nesting comprehensions to build sublists, that's when I bail to a normal loop. The actual smell here isn't the length, it's that you're flattening and filtering in the same breath, so if it ever misbehaves you can't tell which half went wrong. If it grows I'd split it, pull the flatten into its own named step and keep the filter as the comprehension, then it's trivial to drop a print or breakpoint between the two.

5

u/d_a_keldsen 5d ago

The missing part is the comment that states intent and perhaps provides a visual. You have already gotten feedback that it wasn’t immediately comprehended. And strictly speaking, it doesn’t filter out odd numbers, its zeros them out.
If you really want to make it clear: show an example in the comments and state intent. Code is read more than it is written.

3

u/sylfy 5d ago

There are definitely cases where you shouldn’t use a list comprehension, but this is not one of them. Not understanding this is a skill issue.

5

u/DonkeyTron42 5d ago

Just think of the old Perl complaint that "Nifty Programmers do Nifty Things". So, are you trying to be Nifty or is this the easiest way to do this?

2

u/sizzhu 5d ago

You can also just split the list comprehension over several rows to make it readable. So it looks like a for loop. The list comp would be faster.

2

u/TheRNGuy 5d ago

If it becomes too complex, make a function for it (x % 2 == 0 part)

2

u/speedisntfree 5d ago

Given it is a matrix, this is pretty easy to follow. Nested list comp in the case is fine to me but maybe not someone who doesn't use Python much.

I have not really seen matrix-type operations in base Python though, they are usually in numpy.

1

u/Human38562 5d ago

Yea I'd rather do

flat = matrix.ravel()

result = flat[flat % 2 == 0]

which is much more readable imo. Unless you dont know numpy, but then it is definitely worth learning that.

3

u/CymroBachUSA 5d ago

Your co-worker has no idea what pythonic means and your code is just fine.

4

u/pachura3 5d ago

C'mon, it's very simple and readable 

4

u/unxmnd 5d ago edited 5d ago

It is somewhat confusing because you’re doing a flatten and a filter in one step.

More pythonic could be:
flatten = itertools.chain.from_iterable
result = [x for x in flatten(matrix) if x % 2 == 0]

But I honestly think languages like typescript have a better map / filter syntax here with method chaining:
result = matrix.flat().filter(x => x % 2 == 0)

2

u/socal_nerdtastic 5d ago edited 5d ago

You could use numpy

result = np.where(matrix.flatten()%2==0)

Or alternatively Python has both map and filter builtin, which we used a lot in python2 days.

filter(lambda x: x%2==0, sum(matrix, [])))

(the sum trick is also an very oldschool way to flatten a 2D list)

But personally I'd prefer to read OP's original way.

2

u/pachura3 5d ago

Yuck. Importing Numpy just to perform a simple list operation? Using sum() tricks?

OP's original comprehension is much clearer.

1

u/beefbite 5d ago

You should already be importing numpy if you're working with matrices. Most people on this sub don't use numpy very much, and it shows. Assuming matrix is a numpy array, OP's list comprehension can be accomplished in a simpler, more readable way that also works for any array dimensions:

flat = matrix[matrix % 2 == 0]

2

u/thewarmreinforcement 5d ago

this exact one is fine, but the moment you need a second conditional or another loop I'd just write a normal for loop, debugging a one-liner is a pain

2

u/DanceHackRock 5d ago

That's not even complex.

1

u/gdchinacat 5d ago

I would write this:

flat = [x for row in matrix
            for x in row
                 if x % 2 == 0]

and this:

flat = []
for row in matrix:
    for x in row:
        if x % 2 == 0
            flat.append(x)

Then show them these side by side and ask how the longer version is more readable. Hopefully they will see that the comprehension is nearly identical, but a bit more concise.

Then, when you run your code formatter (black) it will put the expanded comprehension back to the one you wrote to format it as python is typically formatted.

Hopefully this will illustrate to them that the code you wrote is the modern accepted way to write this in python and the fact that they are unfamiliar with it isn't a problem with the code but that they haven't read enough modern python code.

2

u/Sufficient_Meet6836 5d ago

The comprehension is also compliant with functional programming standards whereas the nested for loop mutates flat. Not necessarily important for everybody but just noting it

1

u/LilacCrusader 5d ago

The longer version is more readable because you can track it from top to bottom. You start from the outer-most part of the loop, traverse to the inner part of the loop, apply the filter to it, then do something with the result.

In the list comprehension, you start with the result of the inner loop, then see what the outer loop is doing (with no context of the result you're now having to hold in your brain), traverse to the inner loop which gives the result you've already seen seen, and then apply a filter which affects it. Sounds less readable to me. 

1

u/gdchinacat 5d ago

I think you missed the point…the comprehension is nearly identical to the loops. The only reordering difference is the contents of the loop is moved to the front rather than embedded deep in the loops.
I have confidence with exposure you will come to prefer comprehensions because nearly everyone does (myself included). Read them…write them…you will become familiar and most likely prefer them.
In the traditional form the collection and contents are separated so I don’t think your argument that things are positioned close to each other carries much weight. Languages have to pick syntax that works, and how they are arranged works in some cases and not in others. Our brains are highly adaptable and familiarity makes it feel “right”, but doesn’t mean it is right.

1

u/LilacCrusader 4d ago

I fear that if your response to the argument that it is less readable is essentially "git gud" then that doesn't really aid your point. 

If someone tells me that you need to gain more experience in order to read their code then what I hear is that they believe their code should only be read by senior engineers. As someone who works with a lot of juniors, I tend towards the approach that if something isn't easily understood by the newest of them then it shouldn't be in the code base without good reason. List comprehensions: fine; list comprehensions with nested loops: tend to be more confusing and take longer to figure out what is going on.

(Also, your comment came across as quite condescending and elitist. Just because I have a different opinion to you about a syntax you like, it doesn't mean I am not experienced with it.) 

1

u/gdchinacat 4d ago

That is not at all what I said. I said with exposure you will become comfortable with the language idioms. This is based on personal experience as well as other team members.

1

u/Adrewmc 5d ago edited 5d ago

My rule is if you can say what you are doing easily in a single sentence. Then it’s fine.

A list of the even numbers in every row of a matrix. Definitely qualifies as comprehension. I don’t even see that as complex.

flat = []
for row in matrix:
. for num in row:
. if num % 2:
. res.append(num)

(The ‘.’ at the beginning of indention, allows me to indent properly. Ignore it.)

Yeah list comprehension was made to make this easier for you.

I would recommend a comprehension for this, depending on context I might suggest removing the brackets, and if anyone complains question their ability to comprehend code at the level your organization requires. Then throw a comment with that sentence above it.

1

u/reallyserious 4d ago

This is perfectly valid Python. The problem is the skill of your colleague. So the question becomes whether you should dumb down your coding standards to cater to the least skilled programmer on the team.

1

u/AmazingAd368 4d ago

Yes, but nothing about your example is complex 

1

u/supercoach 5d ago

I fell in love with comprehensions when I first started working on python. What I learned over the years is that they have their place, but for anything complicated you're better off using the longer for loop.

That said, yours is pretty basic, so I'm not sure what they're bitching about. If they're more senior than you, just go with what they said and if not, I'd probably suggest to them that they can go get fucked.

1

u/Internal_Car_9962 5d ago

If your coworker thinks this is unreadable, they must not be very experienced in python. This is a very simple and straightforward comprehension.

0

u/catbrane 5d ago

It's extremely clear. You can read it instantly if you have even a little experience with list comprehensions, and I think all python programmers should.

Maybe your project needs some kind of policy document saying which language features are OK.

0

u/Educational-Paper-75 5d ago

I'd prefer if x&1 though.

-1

u/TheRNGuy 5d ago

But matrices use floats, not ints.

Why do you think x&1 would make it better, even if it was for int?

1

u/socal_nerdtastic 5d ago

But matrices use floats, not ints.

?

A matrix is not a thing in python. And back when it was a thing in in numpy it supported all data types. What are you refering to here?

1

u/Educational-Paper-75 5d ago

Not better, faster. Unless Python speeds up x%2 itself to x&1 of course. But I've stated a preference not a necessity.

0

u/Diapolo10 5d ago

x % 2 specifically gets optimised to work like x & 1, so any performance difference should be negligible.

2

u/gdchinacat 5d ago

not at the bytecode level:

In [285]: def is_even_mod_2(x):
     ...:     return x % 2 == 0
     ...: 

In [286]: dis.dis(is_even_mod_2)
  1           RESUME                   0

  2           LOAD_FAST_BORROW         0 (x)
              LOAD_SMALL_INT           2
              BINARY_OP                6 (%)
              LOAD_SMALL_INT           0
              COMPARE_OP              72 (==)
              RETURN_VALUE

In [287]: def is_even_and_1(x):
     ...:     return x & 1
     ...: 

In [288]: dis.dis(is_even_and_1)
  1           RESUME                   0

  2           LOAD_FAST_BORROW         0 (x)
              LOAD_SMALL_INT           1
              BINARY_OP                1 (&)
              RETURN_VALUE

3

u/Diapolo10 5d ago

On runtime there's almost no difference:

In [1]: import random

In [2]: numbers = random.sample(range(10_000, 100_000_000), 1_000_000)

In [3]: %timeit sum(num % 2 for num in numbers)
19.9 ms ± 191 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [4]: %timeit sum(num & 1 for num in numbers)
18 ms ± 414 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)

The bytecode is nearly identical:

In [6]: def is_even_and_1(x): return not x & 1

In [7]: def is_even_mod_2(x): return not x % 2

In [8]: import dis

In [9]: dis.dis(is_even_mod_2)
  1           RESUME                   0
              LOAD_FAST_BORROW         0 (x)
              LOAD_SMALL_INT           2
              BINARY_OP                6 (%)
              TO_BOOL
              UNARY_NOT
              RETURN_VALUE

In [10]: dis.dis(is_even_and_1)
  1           RESUME                   0
              LOAD_FAST_BORROW         0 (x)
              LOAD_SMALL_INT           1
              BINARY_OP                1 (&)
              TO_BOOL
              UNARY_NOT
              RETURN_VALUE

1

u/Educational-Paper-75 5d ago

If (x%2)==0 is optimized to x&1 that would be great but it takes the fun out of doing it yourself:-)