A "for ... if" statement

What do people think about a simple (in theory?) addition to the for loop to make the following valid Python:

for val in iterator if predicate(val):
    # do stuff with val

This would be equivalent to

for val in iterator:
    if predicate(val):
        # do stuff with val

The closest you can get currently I think is

for val in (val for val in iterator if predicate(val)):
    # do stuff with val

or

for val in filter(predicate, iterator):
    # do stuff with val

The first form feels just a little bit awkward. The second form loses its elegance if your predicate isn’t already a convenient function.

The main motivation is to save a level of indentation and be able to express intent a bit more directly.

2 Likes

This has been discussed many times, including these:

if-statement in for-loop

if-syntax for regular for-loops

for-loop-if like list comps have?

Syntax proposal of for…in…if in regular for loops

The benefit is negligible:

  • you save one line, and one indent level;

  • new lines are cheap, who cares about one line?

  • indents are not so cheap, but if your code has so many indents that this becomes important, you probably should refactor your code.

Often, you can’t use this, because the line length will exceed your style guide’s limit on line length, so you will need to split into two lines anyway:

Even if your style guide allows unreasonably long lines, long lines are much harder to read so this will hurt readability, not help it.

Simple cases might be okay:

for obj in seq if cond:
    ...

but more realistic examples are not so good:

for ext in ('.py', '.pyc', '.pyo') if os.path.isfile(os.path.join(path, '__init__' + ext)):
    ...

The for...if line is 91 columns. Too long! Put it inside a method of a class, and you have 99 columns. Even worse.

So my opinion is:

  • one more special case for people to learn about Python’s syntax;

  • usually hurts readability, not helps;

  • more complicated language rules, for negligible benefit.

The big difference between this proposal and list comprehensions is not the “save one line” part, but that comprehensions are expressions that can be used in other parts of code, but this is a statement.

When I have long comprehensions, I almost always split them over multiple lines, and usually at the “if”:

result = function(arg, obj,
            [long_expression for x in some_sequence
             if some_long_condition],
            key=something)

so saving a line in a loop is just not that important to me.

6 Likes

The indent cost is negligible if you do this

for val in iterator:
  if not predicate(val):
    continue
  do_something(val)
3 Likes

Its half as much, but the cost to readability, greater ease of introducing and difficulty spotting bugs due to mistaken intent levels, and lack of conformance with standard Python conventions is much less negligible.

I don’t think Michel was referring to halving the indent to 2 spaces,
but to reversing the sense of the test.

# instead of this
for obj in items:
    if condition:
        block  # two indents

# reverse the test
for obj in items:
    if not condition:
        continue
    block  # one indent

Either style is of course perfectly Pythonic, and a matter of taste.

4 Likes

Ah yes, of course. My silly mistake; I should have examined it more carefully—indeed, I typically prefer that style myself, at least for for blocks longer than a few lines.

1 Like

Thanks for the feedback! I think I’m going to live with doing something like:

iterator_i_want = (obj for obj in iterator_i_have if predicate(obj))
for obj in iterator_i_want:
    do_stuff(obj)

Personally, I’m a bit allergic to continues

To be more specific, that’s a generator (which is a type of iterator). And in that case, why not just

for obj in (obj for obj in iterator_i_have if predicate(obj)):
    do_stuff(obj)

Its shorter, simpler, and closer to your ideal syntax.

That’s up to you; IIRC there are some people who prefer not to use break and continue as they feel they make the control flow less predictable and harder to follow, ala goto. Personally, at least when they are used in predictable places (at the top or bottom of the loop), they don’t tend to pose a practical problem in that way. And if its between using them or multiple layers of nesting (which can lead to easy to make and hard to catch bugs, that judging by how many users on the help forum here make them, seems like a bigger concern), I’d usually take the break/continue. But I do tend to avoid them if there’s other equally readable and performant ways to structure the code.

1 Like

I did propose that in the original post, and in most cases I don’t mind it. I guess my point was, in cases where that feels a bit noisy (which are the cases I wanted the new syntax for), splitting the generator expression into its own line is a reasonable enough solution that doesn’t require any changes to the language.

1 Like

Another 2 years have passed and I was looking for news on for x in foobar if y:, yet all I see here are eye-sore-inducing workarounds with silly arguments that defy everything pythonic

for obj in (obj for obj in iterator_i_have if predicate(obj)):? seriously? What about this is “Simple is better than complex”? “Readability counts”! “Beautiful is better than ugly”!? “Flat is better than nested”!

for val in iterator: if not predicate(val): continue do_something(val)
Same as above! Flat is better than nested!? How does having the “continue” improve readability?

Often, you can’t use this, because the line length will exceed your style guide’s limit on line length, so you will need to split into two lines anyway

And if the line length is nowhere to be reached? I call upon pathlib — Object-oriented filesystem paths — Python 3.12.3 documentation!

>>> p = Path('.')
>>> [x for x in p.iterdir() if x.is_dir()]

This is the kind of example we are dealing with! Observe, how this is a generator - any reasonable person would assume that removing the leading “x” and stripping the [ and ] would result in a valid for-loop!

Alas, we have a generator expression that DOES EXACTLY WORK LIKE ... for x in foobar if y, so anyone who sees “for x in foobar” MUST assume ... if y should work here as well - so NOT having it is actually a special case! How about “Special cases aren’t special enough to break the rules. Although practicality beats purity.” Is NOT having the conditional as part of the loop improving practicality? NOPE.

2 Likes

How about “There should be one-- and preferably only one --obvious way to do it.”?

for value in values if value >= 0:
    ...

for value in values:
    if value >= 0:
        ...

Also, imagine you want to add extra code, now you have to use a regular for loop again:

for value in values:
    ...
    if value >= 0:
        ...
    ...

“There should be only one obvious way to do it” - Exactly! The basic semantics of the generator expression mirrors that of a for-loop, for good reason - it’s the obvious way. But it’s not obvious why the for-loop syntax doesn’t mirror that of a generator!

for value in values:
…
if value >= 0:
…

In case of a generator, you would have to break the code open just the same. No argument there.

Let’s be honest. Generators have been more sexy than plain for-loops in the past, so it got more attention and more features. Before generators, nobody would have dared to suggest anything about changing how for-loops work because that’s how it’s always been, right? So now we’re in a strange place - the new kid on the block (generators) has grown into an adult and the old geezer (for-loop) needs to learn new tricks to keep up.
The only reason why this hasn’t been realized already ages ago was because nobody cares enough to make it happen, otherwise this would be a nobrainer.

You’re still suggesting a new way to do it, that doesn’t allow for anything new. And how exactly do for loops not mirror generators? You simply don’t use : or result.append():

result1 = [
    value
    for values in values_list
    for value in values if value >= 0
]
result2 = []
for values in values_list:
    for value in values:
        if value >= 0
            result2.append(value)

It’s different as this would just be used to decrease indentation. For a similar reason docstrings use """...""" and not "..." otherwise you would need to change it when you want to add a second line.

Using the zen of python as a argument for or against a proposal is not a good argument. It’s a poem (written 20+ years ago), not actual guidelines, and almost every line can be interpreted in a way that goes for or against any proposal. To me it seems most people are ok or prefer the current way where you check inside the loop and then break or continue. Appealing to the zen is unlikely to change their minds.

4 Likes

I sometimes use good old filter when I have a predicate that takes a single argument

for x in filter(None, y):
    ...
# equivalent to
for x in y:
    if not x:
        continue

for char in filter(str.isupper, string):
    ...
3 Likes

Now try that with a simple comparison like n > 3… :wink:

personally I find the generator statements with if in them a bit hard to read.

Is there a reason why we can’t have

for obj in iterator: if condition(obj):
    do_stuff(obj)

?
(Besides the fact that formatters would likely disable such formatting for people like me)

The very fact that someone proposed using a

if not condition:
    continue

statement proofs to me that there is something slightly wrong about the current situation.

This topic was created in 2022, never got traction, but keeps getting revived. Don’t revive old abandoned topics.