Breaking/continuing out of multiple loops

Hi!

SUGGESTION

An easy way to break/continue within nested loops.

MY REAL LIFE EXAMPLE

I am looping through a list of basketball players, each of which has multiple tables, each of which can have up to three different versions. I manipulate data in the deepest loop and, if some key data is missing, I want to continue or break out of the nested loops because I am no longer interested in the player.

PROPOSED NOTATION

for player in all_players:
	for player_tables in all_tables:
		for version in player_tables:
			# things have gone wrong, need to break
			break 2
		
		this_is_not_reached = True

	this_line_is_called()


for sport in all_sports:
	for player in all_players:
		for player_tables in all_tables:
			for version in player_tables:
				# things have gone wrong, go to next iteration of all_sports loop
				continue 4
		
			this_is_not_reached = True

		this_is_not_reached = True

FINAL REMARKS

Maybe this isn’t exactly the most world-changing addition but this is not the first time that I could have used it in real life. I imagine it shouldn’t be very hard to implement too!

Thanks!

2 Likes

Would this suggestion also apply to nested while loops, and to nested combinations of for and while loops?

for ...
    ...
    while ...
        ...
        for ...
            ...
            break 3 # breaks out of all of the loops
            ...
        ...
    ....


Variations of this were proposed and rejected before, see:

You would presumably need at least some very convincing examples that outweigh the reasons given in the rejection notice.

Yes, I would propose so.

I don’t know how to reply to two posts at the same time in this forum, but in response to Peter Suter’s post:

That’s too bad! Since it was rejected 15 years ago, perhaps it’s time to reconsider. To be perfectly honest, I am nowhere near sufficiently qualified to make an educated argument defending how its merits would compare to the onus of the necessary changes.

What I can say, though, is that the refactoring approaches that I found people suggesting to get around such cases seem to me infinitely clumsier than the simple addition of an integer. So clumsy, in fact, that it seemed to me very anti-pythonic, which is why I thought I might suggest this here.

Anyway, hopefully this will encourage more competent people than I to talk about it and, hopefully, implement it.

4 Likes

[…] than the simple addition of an integer.

If you were going to do it then I’d argue for anything but an integer. Your code will be very easily broken by anyone refactoring to add or remove a loop.

I’m not convinced it’s really needed though. Much better to add goto to the language :wink:

2 Likes

True, but I’d argue that any refactoring that adds or removes a whole loop is significant enough to justify simply changing the value of this integer as well since it would be incredibly fast and easy

1 Like

Hi PythonMillionaire,

I think it’s cheapest to raise something (such as StopIteration) from a nested loop and catch it in the outermost loop. No nest counting, neither goto :wink:

1 Like

I actually kind of agree with that. For example the top result when I search “python multiple for loop break” is:

The first response has no example, so is maybe easily ignored, the second response has a clever solution, but maybe “too clever”? Applied to the original example above it would look like this I guess:

for sport in all_sports:
	for player in all_players:
		for player_tables in all_tables:
			for version in player_tables:
				# things have gone wrong, go to next iteration of all_sports loop
				break
			else:
				continue
			break
		else:
			continue
		break

:face_with_spiral_eyes:

3 Likes

Hi Kazuya!

Yes, I saw this among the suggestions but it’s just so much clumsier than adding one single character to break any loop no matter how many there might be. All other approaches are also comparatively much more complicated.

ADVANTAGES

  1. It probably doesn’t break any existing programs
  2. It saves time
  3. It’s super-tasty syntactic sugar that looks cleaner by far and wide than any other alternative.
  4. It’s just so pythonesque. Come on, it’s the same beautiful simplicity of, say, list comprehensions. Part of Python’s charm is doing things in as few lines as possible, in as elegant a manner as possible.

Even if this isn’t going to be used in every project in the world, it still will be used enough to justify it. In addition to having wanted this feature myself a few times in the past, I have seen a bunch of people asking how this can currently be done online and the proposal demonstrates how even in 2007, when the userbase was so much smaller, there were already people who would like to use it.

There’s even a Medium article teaching 5 poor ways of doing this. All are incomparably worse:
5 Ways To Break Out of Nested Loops in Python | by Yang Zhou | TechToFreedom | Medium

1 Like

Reading the rejection notice it seems very subjective (and Guido partially admits to this): [Python-3000] Announcing PEP 3136

In fact in more recent years I’ve core devs / steering council members / Guido say that arguing a “feature will be abused” is subjective and not sufficient reason to dismiss something.

I would guess with a proposed PEP that is more clear in it’s specific solution and gives clear examples of where it’s helpful it would at least be viable to discuss.

3 Likes

Time alone doesn’t mean anything. A number of decisions made back in the Python 1.x days are still important today. Not sure what you mean by “educated argument” - all you have to do is explain why the rejection should be revisited :slight_smile: Demonstrate that the objections given at the time are no longer applicable, or that there are other good reasons to reconsider this.

Have a look at other languages that have ways of breaking out of multiple loops. Do they label the loops or count them? How frequently is the feature used well, and how frequently is it abused? (Yes, this is subjective.)

Remember that a good number of multi-break situations can be better handled by treating the entire loop as a search operation, refactoring it into a function, and having the multi-break become “cool, we found the thing, return a result”. So you have to also show cases where this would not be appropriate.

2 Likes

‘try:’ has always been fast and I believe it became even faster, or even free at runtime in 3.11 (or possibly 3.12) due to better compilation. It is already Python’s general ‘break execution’ mechanism. The need for multiple-loop breaks is really much rarer than that of single loop breaks, so I don’t think we need a special form for the latter.

The obvious fix for that ugly code is to refactor into a function:

def handle_inner_loops(sport):
    for player in all_players:
        for player_tables in all_tables:
            for version in player_tables:
                if condition:
                    # things have gone wrong, bail out early.
                    return
                block()

for sport in all_sports:
    handle_inner_loops(sport)

The solution to “Python needs a way to jump out of a chunk of code” is usually to put the chunk of code into a function, then return out of it.

I don’t know what refactoring approaches you are thinking of, but if you think putting code in a function is “anti-pythonic”, I think you’re going to have a bad time :slight_smile:

Otherwise we can simulate some kinds of goto using a raise:

class MultiBreak(Exception): pass

for sport in all_sports:
    try:
        for player in all_players:
            for player_tables in all_tables:
                for version in player_tables:
                    if condition:
                        # things have gone wrong, bail out early.
                        raise MultiBreak
                    block()
    except MultiBreak:
        pass

The good thing here is that you can see exactly where you are jumping to, and if you need to do some special handling it is easy to replace the pass with the error recovery code.

6 Likes

You have missed the point that if you are adding or removing nested loops, all the break 3 numbers will need to be changed or you will silently break out too many or two few levels. And that is easy to miss.

break 3 as you have it tells the interpreter to break three times, which is fine, but it makes it hard for the reader and writer to tell what that means. After you have done the break three times, where in the code are you?

You have to count loops, and that is easy to get wrong.

A break (or continue) is effectively a safe goto. continue is effectively “goto the start of the current loop” and break is effectively “goto the end of the current loop”. So we can salvage this numbered break idea by using labels instead of having to count loops.

for obj in sequence as A:
    for item in another_sequence:
        for x in stuff as B:
            for y in things:
               for z in stuffythings:
                   if condition:
                       break B  # Jump to just after loop B
                   elif something:
                       continue A  # Jump to the start of loop A

The labelled break and continue tell us exactly where you are jumping to, without having to count loops and wonder whether we got the right number or counted wrong. Adding or removing a loop automatically works, without needing to change the labels.

8 Likes

Another solution is to refactor in the opposite direction, by moving the nested loops into a generator which yields the values. You could also in-line it as a generator expression, or use something like itertools.product().

Remember that a good number of multi-break situations can be better handled by treating the entire loop as a search operation, refactoring it into a function, and having the multi-break become “cool, we found the thing, return a result”. So you have to also show cases where this would not be appropriate.

Actually I don’t agree that this needs to be shown for the same reason that for loops don’t need to be shown to be inappropriate for list comprehensions to be deemed useful. Given that what I am proposing is syntactic sugar, the only thing that does need to be shown, IMO, is that this is a good suggestion as far as syntactic sugar goes since nobody would ever be required not to use any previous methods

‘try:’ has always been fast and I believe it became even faster, or even free at runtime in 3.11 (or possibly 3.12) due to better compilation. It is already Python’s general ‘break execution’ mechanism.

Yes, it works but it’s far uglier:

  • You need to look at the except blocks to understand why they are there if you didn’t write the program
  • They are ambiguous at best since try-except’s main purpose is not this
  • It adds another tab level for each place you want to stop, which pushes your code farther to the right and is never ideal
  • It’s a lot more work to remove from code than the notation I am proposing
  • Much more importantly, there would need to be ONE try-except block FOR EACH LEVEL in order to achieve the same leve of control plus two excepts, one for continue and one for break

The need for multiple-loop breaks is really much rarer than that of single loop breaks, so I don’t think we need a special form for the latter.

Well, this suggestion has come up multiple times throughout the years and you can find a lot of threads and Reddit posts asking about this subject online. This shows that, even if it happens a lot less frequently, it does still happen a good deal. Not to mention, there are multiple languages that did deem this useful enough to implement

The obvious fix for that ugly code is to refactor into a function

This is exactly one of the reasons why we need this syntactic sugar. If something must be refactored in order for you to achieve relatively basic functionality, I think that this is, at least, not ideal.

Not to mention this solution does not include the possibility of also using “continue” at any level, as I am proposing.

As for try and except, this approach is simultaneously compatible with both break and continue if we raise two different exceptions but is this really what try-except was meant for? For the reasons I stated above, I think that this notation is far less clear and certainly not ideal

You have missed the point that if you are adding or removing nested loops, all the break 3 numbers will need to be changed or you will silently break out too many or two few levels. And that is easy to miss

Adding or removing nested loops is a big change to the code. Something pretty big needs to have changed for you to do decide to completely change the flow of your program. Compared to that, changing a single number should be trivial.

break 3 as you have it tells the interpreter to break three times, which is fine, but it makes it hard for the reader and writer to tell what that means. After you have done the break three times, where in the code are you?

I do agree that label breaks are better in this one particular respect but I don’t necessarily think that they would be better as the sole possible notation. I think numbers suffice but both notations could be offered if there is no technical impediment. At the very least, I would argue that numbered breaks are much, much easier than telling where you you are in the code if we use one of the currently available notations:

for sport in all_sports:
    try:
        for player in all_players:
            try:
                for player_tables in all_tables:
                    try:
                        for version in player_tables:
                            if condition:
                                # things have gone wrong, bail out early.
                                raise MultiBreak
            
                                block()
            
                    except MultiBreakThree:
                            break
                    
                    except MultiContinueThree:
                            break
                        
            except MultiBreakTwo:
                break

            except MultiContinueTwo:
                break
                
    except MultiBreakOne:
            break

    except MultiContinueOne:
        break
1 Like

Is there an example of a language that has implemented this feature? (Not counting generic goto.) It would do this suggestion well to describe the design decisions, if the developers regret any decisions, and if the users use or avoid the feature.

The only one I know is Ada (which calls it exit). You can provide the loop name to break/exit from another than the inner loop

There may be more, couldn’t find a list by simple Google

1 Like

This list has many languages that have a (not necessarily multi-level) break.

Some popular ones have labeled break and continue:

C# did not (yet) add it, but one main reason seems to be that it already has a goto. Discussion

Posted above was PHP with numeric break.

2 Likes

A multi-break with “level numbers” instead of named labels would a usability nightmare, since it wouldn’t be possible to quickly locate the target of your fancy goto by means of a simple code grep.

1 Like