for / while else doesn't make sense

Chris Angelico rosuav at gmail.com
Sat May 21 03:29:20 EDT 2016


On Sat, May 21, 2016 at 5:20 PM, Erik <python at lucidity.plus.com> wrote:
> On 20/05/16 01:06, Steven D'Aprano wrote:
>
>> In my experience, some people (including me) misunderstand "for...else" to
>> mean that the else block runs if the for block *doesn't*. It took me the
>> longest time to understand why this didn't work as I expected:
>>
>> for x in seq:
>>      pass
>> else:
>>      print("seq is empty")
>
>
> So why don't we consider if that should be a syntax error - "else" clause on
> "for" loop with no "break" in its body? I know that doesn't change your
> fundamental mental model, but it's a hint that it's wrong :)
>
> As it's backwards-incompatible, it could be introduced using a __future__
> import (a precedent is 'generators' and the "yield" keyword back in the day)
> which those who would like the new check could add to the top of their
> sources.
>
> But also, as you say, any instances of that construct in the wild is almost
> certainly a bug, so it would be good to be able to test code using a
> command-line or similar switch to turn on the behaviour by default for
> testing existing bodies of code.
>
> I notice that pylint complains about this (as a warning). Is there any
> reason why this should _not_ just be considered an error and be done with
> it?

Because it's not the language parser's job to decide what's "sensible"
and what's "not sensible". That's a linter's job. Some things
fundamentally can't work, but others work fine and might just be
not-very-useful - for example:

def f(x):
    x if x <= 2 else x * f(x-1)

This is syntactically-legal Python code, but it probably isn't doing
what you want it to (in Python, a bare final expression does NOT
become the function's return value, although that does happen in other
languages). A linter should look at this and give a warning (even if
the else part has side effects, the first part can't, and the
condition shouldn't), but the language will happily evaluate it (this
example from CPython 3.6):

>>> dis.dis(f)
  2           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (2)
              6 COMPARE_OP               1 (<=)
              9 POP_JUMP_IF_FALSE       18
             12 LOAD_FAST                0 (x)
             15 JUMP_FORWARD            17 (to 35)
        >>   18 LOAD_FAST                0 (x)
             21 LOAD_GLOBAL              0 (f)
             24 LOAD_FAST                0 (x)
             27 LOAD_CONST               2 (1)
             30 BINARY_SUBTRACT
             31 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             34 BINARY_MULTIPLY
        >>   35 POP_TOP
             36 LOAD_CONST               0 (None)
             39 RETURN_VALUE

So, yes, this function might load up the value of x (position 12),
then jump to 35 and discard that value, before unconditionally
returning None. Not a problem. It's up to the linter, and ONLY the
linter, to tell you about this.

ChrisA



More information about the Python-list mailing list