PEP: 548 Title: More Flexible Loop Control Version: $Revision$
Last-Modified: $Date$ Author: R David Murray Status: Rejected Type:
Standards Track Content-Type: text/x-rst Created: 05-Sep-2017
Python-Version: 3.7 Post-History: 05-Aug-2017

Rejection Note

Rejection by Guido:
https://mail.python.org/pipermail/python-dev/2017-September/149232.html

Abstract

This PEP proposes enhancing the break and continue statements with an
optional boolean expression that controls whether or not they execute.
This allows the flow of control in loops to be expressed more clearly
and compactly.

Motivation

Quoting from the rejected PEP 315:

  It is often necessary for some code to be executed before each
  evaluation of the while loop condition. This code is often duplicated
  outside the loop, as setup code that executes once before entering the
  loop:

      <setup code>
      while <condition>:
          <loop body>
          <setup code>

That PEP was rejected because no syntax was found that was superior to
the following form:

    while True:
        <setup code>
        if not <condition>:
            break
        <loop body>

This PEP proposes a superior form, one that also has application to for
loops. It is superior because it makes the flow of control in loops more
explicit, while preserving Python's indentation aesthetic.

Syntax

The syntax of the break and continue statements are extended as follows:

    break_stmt : "break" ["if" expression]
    continue_stmt : "continue" ["if" expression]

In addition, the syntax of the while statement is modified as follows:

    while_stmt : while1_stmt|while2_stmt
    while1_stmt : "while" expression ":" suite
                  ["else" ":" suite]
    while2_stmt : "while" ":" suite

Semantics

A break if or continue if is executed if and only if expression
evaluates to true.

A while statement with no expression loops until a break or return is
executed (or an error is raised), as if it were a while True statement.
Given that the loop can never terminate except in a way that would not
cause an else suite to execute, no else suite is allowed in the
expressionless form. If practical, it should also be an error if the
body of an expressionless while does not contain at least one break or
return statement.

Justification and Examples

The previous "best possible" form:

    while True:
        <setup code>
        if not <condition>:
            break
        <loop body>

could be formatted as:

    while True:
        <setup code>
        if not <condition>: break
        <loop body>

This is superficially almost identical to the form proposed by this PEP:

    while:
        <setup code>
        break if not <condition>
        <loop body>

The significant difference here is that the loop flow control keyword
appears first in the line of code. This makes it easier to comprehend
the flow of control in the loop at a glance, especially when reading
colorized code.

For example, this is a common code pattern, taken in this case from the
tarfile module:

    while True:
        buf = self._read(self.bufsize)
        if not buf:
            break
        t.append(buf)

Reading this, we either see the break and possibly need to think about
where the while is that it applies to, since the break is indented under
the if, and then track backward to read the condition that triggers it;
or, we read the condition and only afterward discover that this
condition changes the flow of the loop.

With the new syntax this becomes:

    while:
        buf = self._read(self.bufsize)
        break if not buf
        t.append(buf)

Reading this we first see the break, which obviously applies to the
while since it is at the same level of indentation as the loop body, and
then we read the condition that causes the flow of control to change.

Further, consider a more complex example from sre_parse:

    while True:
        c = self.next
        self.__next()
        if c is None:
            if not result:
                raise self.error("missing group name")
            raise self.error("missing %s, unterminated name" % terminator,
                             len(result))
        if c == terminator:
            if not result:
                raise self.error("missing group name", 1)
            break
        result += c
    return result

This is the natural way to write this code given current Python loop
control syntax. However, given break if, it would be more natural to
write this as follows:

    while:
        c = self.next
        self.__next()
        break if c is None or c == terminator
        result += c
    if not result:
        raise self.error("missing group name")
    elif c is None:
        raise self.error("missing %s, unterminated name" % terminator,
                         len(result))
    return result

This form moves the error handling out of the loop body, leaving the
loop logic much more understandable. While it would certainly be
possible to write the code this way using the current syntax, the
proposed syntax makes it more natural to write it in the clearer form.

The proposed syntax also provides a natural, Pythonic spelling of the
classic repeat ... until <expression> construct found in other
languages, and for which no good syntax has previously been found for
Python:

    while:
        ...
        break if <expression>

The tarfile module, for example, has a couple of "read until" loops like
the following:

    while True:
        s = self.__read(1)
        if not s or s == NUL:
            break

With the new syntax this would read more clearly:

    while:
        s = self.__read(1)
        break if not s or s == NUL

The case for extending this syntax to continue is less strong, but
buttressed by the value of consistency.

It is much more common for a continue statement to be at the end of a
multiline if suite, such as this example from zipfile :

    while True:
        try:
            self.fp = io.open(file, filemode)
        except OSError:
            if filemode in modeDict:
                filemode = modeDict[filemode]
                continue
            raise
        break

The only opportunity for improvement the new syntax would offer for this
loop would be the omission of the True token.

On the other hand, consider this example from uuid.py:

    for i in range(adapters.length):
        ncb.Reset()
        ncb.Command = netbios.NCBRESET
        ncb.Lana_num = ord(adapters.lana[i])
        if win32wnet.Netbios(ncb) != 0:
            continue
        ncb.Reset()
        ncb.Command = netbios.NCBASTAT
        ncb.Lana_num = ord(adapters.lana[i])
        ncb.Callname = '*'.ljust(16)
        ncb.Buffer = status = netbios.ADAPTER_STATUS()
        if win32wnet.Netbios(ncb) != 0:
            continue
        status._unpack()
        bytes = status.adapter_address[:6]
        if len(bytes) != 6:
            continue
        return int.from_bytes(bytes, 'big')

This becomes:

    for i in range(adapters.length):
        ncb.Reset()
        ncb.Command = netbios.NCBRESET
        ncb.Lana_num = ord(adapters.lana[i])
        continue if win32wnet.Netbios(ncb) != 0
        ncb.Reset()
        ncb.Command = netbios.NCBASTAT
        ncb.Lana_num = ord(adapters.lana[i])
        ncb.Callname = '*'.ljust(16)
        ncb.Buffer = status = netbios.ADAPTER_STATUS()
        continue if win32wnet.Netbios(ncb) != 0
        status._unpack()
        bytes = status.adapter_address[:6]
        continue if len(bytes) != 6
        return int.from_bytes(bytes, 'big')

This example indicates that there are non-trivial use cases where
continue if also improves the readability of the loop code.

It is probably significant to note that all of the examples selected for
this PEP were found by grepping the standard library for while True and
continue, and the relevant examples were found in the first four modules
inspected.

Copyright

This document is placed in the public domain.