Add optional `if break:` suite to `for/while`

--------------------------------------------------------------------------
EDIT 2024-11-24 Sunday

Chenged the proposal from if break: (a novel juxtaposition of two keywords) to if_break: (a single new soft keyword). The former did some damage by giving if and break meanings in this context they have nowhere else. The latter is explicit about that they’re intended to be read as a single unit in this context.

Downsides include that I have no experience implementing soft keywords, so can’t really help whoever (if anyone) finds this interesting enough to implement, and that it will take time for syntax colorers (or more generally code-aware tools) to recognize the new soft keyword.
--------------------------------------------------------------------------

Search loops in Python are often written like so:

for ...: # or while
    if condition found:
        # do _everything_ needed if it's found
        break
else:
    # do everything needed if it's not found

The proposal is to add a new, optional clause, to handle the “if it’s found” case in a clearer way:

for ...: # or while
    if condition found:
        break
if_break:
    # do everything needed if it's found
else:
    # do everything needed if it's not found

if_break is a new soft keyword, acting as a keyword only as part of loop syntax. Elsewhere it would just be (as it is now) an identifier.

Then:

  • The search loop body is solely devoted to searching - better separation of concerns.
  • The “found” and “not found” suites are at the same indentation level. Sometimes the “it’s found” condition is discovered in several layers of nested testing, pushing the important part “far off to the right”.
  • It’s possible that some cases of breaking from multiple loop levels would be easier, although I expect that “many levels” of this would still be better (more readably) done via the “put the whole thing in a try block and raise a custom exception to break out of deep nesting” approach.
for ...:
    while ...:
        if condition:
            break # breaks out of "while"
    if_break:
        break # if & only if "break" in "while" was executed
  • Mnemonic value for those who have a hard time remembering whether else: is entered if a break occurred, or didn’t occur.

if_break: and else: are both optional. Nothing about the semantics of current code would change, and if_break: is currently a syntax error. Over time, I’d migrate much of my own code to use if_break:, but in very simple cases wouldn’t change any of my code. That’s me. YMMV.

Not a big deal, just a “nice to have”. I don’t intend to implement it, but suggest it would be a relatively easy addition for an aspiring core dev to tackle.

31 Likes

I’m afraid this faces an uphill battle against

found = False
for ...: # or while
    if condition found:
        found = True
        break
if found:
    # do everything needed if it's found ...
else:
    # do everything needed if it's not found

The proposal saves two lines and a name, at the cost of confusion when someone tries if break: independent of a loop construct.

6 Likes

Yes, that’s the kind of thing I sometimes resort to now. Code can, of course, be much more complex than my bare-bones skeleton, and it’s very easy to forget to add found = True before every break. I’m unsatisfied with that approach in real life.

Why would they? :wink: They don’t try to use a bare else: independent of a loop construct either. Or, if they do, they get a syntax error. Same thing here if they try to use if break: independent of a loop construct.

4 Likes

I see - this works because break is a keyword. You wouldn’t suggest the if nobreak version because that, standalone, is not a SyntaxError.

3 Likes

Usually you can move “do everything needed if it’s found” just before the break. The only exception if it contains break or continue for the outer loop, but this is not very common case.

With how much confusion else after loop causes, this will be even more obscure part of Python syntax.

2 Likes

While we’re at it, how about add an argument to break (the value we found) and then if break as v:

for x in ...:
    if x is good:
        break f(x)
if break as v:
   ...
6 Likes

This can be solved without any disruption by contributing a rule to popular linters (such as ruff) that mandates a “no break” comment every time a for/else construct is used:

else: # no break

I have seen this convention in the wild and it’s also recommended in several highly upvoted answers on SO.

2 Likes

Yes. That’s the pattern explicitly shown in my first skeleton. It conflates the search-loop logic with the “what to do if it’s found” logic, and can end up pushing the “what to do if it’s found” logic to deep indentation levels (in case of nested testing).

That I disagree with. We already have else:, and if break:: is very explicit about when its suite is entered. If there’s also an else: clause, it would be equally clear that its suite would be entered if not break. There’s nothing here that becomes more obscure.

3 Likes

if no break or if not break would be possible, but I would defer that until Tim’s proposal is implemented. if no except and if not except could also be added, if desired.

One inconvenience of this is that given current conditional statement, if break: looks like a start of a new statement.
Given it is a continuation of a previous one, elif might be a better choice.

for ...: # or while
    if condition found:
        break
elif break:
    # do everything needed if it's found ...
else:
    # do everything needed if it's not found
4 Likes

But wouldn’t you expect that to be equivalent to this?

for ...: # or while
    if condition found:
        break
else: # no break
    if break:
        # unreachable
    else:
        # do everything needed if it's not found

Which is something totally different.

5 Likes

No, I would not.

1 Like

That seems unnecessary since the value is already in the loop variable (x in the example above).

Agreed, although I like the elif break version, myself.

1 Like

I wonder a) how often for ... else is found in the wild[1] and b) how many of those cases would actually migrate. Maybe you would, but not everyone would.

As @storchaka said, it seems like this would be a very obscure piece of syntax. Given how rarely most people would encounter it, I’m not sure there’s any spelling that will make it intuitive when it shows up.


  1. which was the inspiration for this thread in the first place, but no one has done the research ↩︎

Doesn’t manually raising a StopIteration (or maybe a custom exception) achieve this in a way that’s not too hard on the eyes?

try:
    for ...:
        for ...:
            if ...:
                raise StopIteration(...)
except StopIteration as _:
    print("found", _.value)
else:
    print("not found")
2 Likes

Yes, that’s unfortunate. I don’t have a perfect solution, though. I’m relying in the end on that if break: is such an unusual combination that it will stick in peoples’ brains. I don’t care nearly so much about how odd it looks at first glance as about how hard it is to forget after it’s learned. if break pretty much means what it appears to mean.

Possibly, although not to me yet. The advantage of if/else is that it faithfully reflects that exactly one of the branches will be taken. elif/else only reflects that at most one (If any) of the branches will be taken.

5 Likes

Could you explain to me why adding a condition to the else would make it execute here?
elif condition is just an alias for else if condition (which is invalid in Python).

while True:
    break
else:
    print("no condition")

while True:
    break
elif break:
    print("condition")

If this assumption is broken, we’re making things even more confusing than they already are.

I have done use-case analysis for this as part of:

  1. Generalize `elif-else` to all compound statements
  2. `while-elif-else`, `for-elif-else`, `try-except-elif-else`

Cpython repo with site-packages from .venv.

  1. try-else-if - 407
  2. for-else-if - 114
  3. while-else-if - 28

Those are with else immediately followed by if (my proposal was adding conditional-like-elif). Thus, numbers for pure else would be much higher.

I do not use while/for-else very much, but when I do, I always feel that something is missing.

It allows you to have a different value for different breaks:

    for x in ...:
        if cond1:
                 break f(x)
        elif cond2:
                 break g(x)
    if break as v:
        ...
1 Like