|
|
Subscribe / Log in / New account

Python multi-level break and continue

By Jake Edge
August 31, 2022

A fairly lengthy discussion of whether there should be a way to break out of (or continue) more than one level of nested loops in Python recently took place in the Ideas category of the language's discussion forum. The idea is attractive, at least in an abstract sense—some other languages support jumping out of multiple loops at once—but it seems unlikely to go anywhere for Python. The barrier to new features is fairly high, for sure, but there is also a need for proponents to provide real-world examples that demonstrate their advantages. That, too, is a difficult bar to clear, as was seen in the discussion.

Idea

A user called "Python Millionaire" posted an example of some loops that they had written to process data about some basketball players; "I want to continue or break out of the nested loops because I am no longer interested in the player". They proposed adding an integer to break and continue statements to specify how many loops to operate on. For example:

    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()

If Python ever gets this feature, though, the (un-Pythonic?) integer mechanism will surely not be part of it. It is terribly fragile when code gets shuffled around, for one thing. Also, as Bryan Van de Ven observed, it would be "a usability nightmare" because it would be difficult "to quickly locate the target of your fancy goto by means of a simple code grep".

This is not the first time the idea has come up; the feature was raised 15 years ago by Matt Chisholm in PEP 3136 ("Labeled break and continue"). The PEP was rejected by Guido van Rossum for a variety of reasons, including a worry that the feature would "be abused more than it will be used right, leading to a net decrease in code clarity". Peter Suter pointed to the PEP in the discussion, noting that Millionaire "would presumably need at least some very convincing examples that outweigh the reasons given in the rejection notice".

PEP 3136 offered several possibilities for the syntax of the feature, and did not choose one, which was another reason Van Rossum rejected it. But it seems clear that the labeled version is seen as the most viable path, even among those who are against adding the feature. A labeled break might look something like the following:

    for a in a_list as a_loop:
        for b in b_list as b_loop:
            if ...
                break a_loop
That break would exit both loops immediately; a labeled continue would go to the next iteration of the named loop.

Millionaire thought that after 15 years it might be time to reconsider the idea. They lamented that the approaches suggested to work around the lack of multi-level break are "infinitely clumsier" and "anti-pythonic". Suter agreed with that to a certain extent, noting the first search result for "python multiple for loop break" is a Stack Overflow answer that is overly clever. Suter adapted it to the original example as follows:

    for sport in all_sports:   # "for sport" loop
	for player in all_players:
	    for player_tables in all_tables:   # "for player_tables" loop
		for version in player_tables:
		    # things have gone wrong, go to next iteration of all_sports loop
		    break
		else:
		    continue
		break
	    else:
		continue
	    break
That uses the else clause for loops, which will execute if no break is used in the loop, thus the loop runs to completion. So if the innermost loop runs to completion, the continue in the else will result in another iteration of the "for player_tables" loop. If the inner loop uses break, however, it will break twice more, all the way back to the "for sport" loop. As can be seen from that convoluted description, the construct is far from readable—or maintainable.

Other ways

There are multiple ways to accomplish what Millionaire is trying to do, some of which were described in the discussion. Using flags is one obvious, perhaps clunky, mechanism, another is to use exceptions, but that may not be much less clunky. Overall, though, several participants thought that the code itself should be refactored in some fashion. Chris Angelico thought that moving the search operation into its own function, which can return once the outcome is known, would simplify things. Steven D'Aprano agreed:

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.

Millionaire thought that requiring refactoring into a function was less than ideal. It is also not possible to implement a multi-level continue that way. Beyond that, Millionaire pushed back on the notion that labeled break/continue was a better syntactic choice in all cases; offering the numeric option too would give the most flexibility. There was little or no support for keeping the numeric version, however.

But the arguments given in support of the feature were generally fairly weak; they often used arbitrary, "made up" examples that demonstrated a place where multi-level break could be used, but were not particularly compelling. For example, "Gouvernathor" posted the following:

    for system in systems:
	for planet in system:
	    for moon in planet.moons:
		if moon.has_no_titanium:
		    break 2 # I don't want to be in a system with a moon with no titanium
		if moon.has_atmosphere:
		    break # I don't want to be in the same planetary system

As D'Aprano pointed out, though, that is hardly realistic; "your example seems so artificial, and implausible, as to be useless as a use-case for multilevel break". He reformulated the example in two different ways, neither of which exactly duplicated the constraints of Gouvernathor's example, however. He also had some thoughts on what it would take to continue pursuing the feature:

To make this proposal convincing, we need a realistic example of an algorithm that uses it, and that example needs to be significantly more readable and maintainable than the refactorings into functions, or the use of try…except (also a localised goto).

If you intend to continue to push this idea, I strongly suggest you look at prior art: find languages which have added this capability, and see why they added it.

Angelico noted that he has used the Pike programming language, which does have a labeled break. He found that he had used the feature twice in all of the Pike code he has written. Neither of the uses was particularly compelling in his opinion; one was in a quick-and-dirty script and the other is in need of refactoring if he were still working on that project, he said. That was essentially all of the real-world code that appeared in the discussion.

Paul Moore suggested that needing a multi-level break may be evidence that the code needs to be reworked; "Think of it in terms of 'having to break out of multiple loops is a code smell, indicating that you should re-think your approach'." Though he questioned the value of doing so, he did offer up a recent example:

I don't think everyone piling in with their code samples is particularly helpful. The most recent example I had, though, was a "try to fetch a URL 10 times before giving up" loop, inside the body of a function. I wanted to break out if one of the tries returned a 304 Not Modified status. A double-break would have worked. But in reality, stopping and thinking for a moment and factoring out the inner loop into a fetch_url function was far better, named the operation in a way that was more readable, and made the outer loop shorter and hence more readable itself.

Workarounds?

Millionaire complained that all of the suggestions that had been made for ways to restructure the code were workarounds of various sorts; "They are all ways of getting around the problem, not actual solutions presented by the programming language, which should be the case." But Oscar Benjamin said that he could not "picture in my mind real maintainable code where labelled break is significantly better than a reorganisation". All he can see in his mind is the feature "being used to extend the kind of spaghetti code that I already wish people didn't write". There is, of course, an alternative: "if real life examples were provided then we could discuss the pros and cons in those cases without depending on my imagination".

Meanwhile, others in the discussion pushed back against the workaround complaint and also reiterated calls for real-world code. Millionaire returned to his earlier basketball example, with a beefed-up version that uses labeled break and continue. While Millionaire seemed to think it was a perfectly readable chunk of code that way, others were less impressed. Angelico questioned some of the logic, while Van de Ven thought it did not demonstrate quite what Millionaire was claiming:

A 50-line loop body and eight levels of indentation (assuming this is inside a function) and this is the good version? Having a multi-break won't didn't fix that. All the complexity in that code stems from trying to do ad-hoc relational querying with imperative code, at the same time as pre- and post-processing.

Van de Ven and Millionaire went back and forth a few times, with Millionaire insisting that Van de Ven's refactorings and other suggestions were not mindful of various constraints (which were never mentioned up front, of course). Van de Ven thought that the episode was an example of an XY problem, where someone asks about their solution rather than their problem, but he still persisted in trying to show Millionaire alternative ways to structure their code. There are, seemingly, several avenues that Millionaire could pursue to improve their code overall, while also avoiding the need for multi-level break—if they wished to. But that is apparently not a viable path for Millionaire.

The discussion was locked by David Lord shortly thereafter; it was clear that it had run its course.

The convoluted examples presented in the thread were not particularly helpful to the cause, in truth. Users who want to add a feature to Python should have an eye on compelling use cases from the outset, rather than generalized feelings that "this would be a nice addition" to the language. If, for example, code from the standard library had been shown, where a multi-level break would have significantly improved it, the resurrected feature idea might have gained more traction. There are lots of other huge, open Python code bases out there, as well; any of those might provide reasonable examples. So far, at least, no one has brought anything like that to the fore.

This is something of a recurring theme in discussions about ideas for new Python features. To those who are proposing the feature, it seems like an extremely useful, rather straightforward addition to the language, but the reception to the idea is much different than expected. Python developers need to cast a critical eye on any change to the language and part of that is to determine whether the benefit outweighs the substantial costs of adopting it. That is not going to change, so it makes sense for those who are looking to add features to Python to marshal their arguments—examples—well.


Index entries for this article
PythonEnhancements


to post comments

Python multi-level break and continue

Posted Sep 1, 2022 0:03 UTC (Thu) by k8to (guest, #15413) [Link]

I think this is a rare problem, and that exceptions handle it just fine, just be sure to use a very specific exception. Having a class intended for this use might help.

Python multi-level break and continue

Posted Sep 1, 2022 0:22 UTC (Thu) by dskoll (subscriber, #1630) [Link] (17 responses)

I don't program in Python, but Perl does this with labels:

OUTER:
  for (my $i=0; $i<10; $i++) {
    for (my $j=0; $j<10; $j++) {
        if (ignore_this_j($j)) {
          next;  # Continues the inner loop
        }
        if (ignore_remaining_js($i, $j)) {
          next OUTER;  # Continues on the next $i loop
        }
    }
  }

If Python has labels, this would seem a decent mechanism.

Python multi-level break and continue

Posted Sep 1, 2022 1:16 UTC (Thu) by k8to (guest, #15413) [Link] (14 responses)

Yeah, I've used then in C and C++ for this type of jump out goal. It works fine so long as you don't make a tangle with RAII.

Python doesn't have labels or the attendant goto. continue exists to go to the next interation, but only the innermost loop.

Python multi-level break and continue

Posted Sep 1, 2022 1:34 UTC (Thu) by dtlin (subscriber, #36537) [Link] (3 responses)

Javascript, Java, Kotlin, Swift, and Rust have break/continue of labeled loops, but no goto. In Rust's case, this plays fine with RAII.

Python multi-level break and continue

Posted Sep 1, 2022 7:53 UTC (Thu) by tialaramex (subscriber, #21167) [Link] (2 responses)

I don't believe k8to was suggesting the problem is that C++ RAII itself gets tangled in the loop break, but that you as a programmer may get confused, which would potentially also be a problem in Rust. In (safe) Rust you can't cause unsafety as a result, but you might still end up with the wrong mental picture.

Rust's break can instead take a value (where appropriate) so that "break 2;" is actually valid Rust although it doesn't do what's proposed for Python (it means the inner loop terminates immediately with the value 2)

Python multi-level break and continue

Posted Sep 1, 2022 22:26 UTC (Thu) by dtlin (subscriber, #36537) [Link] (1 responses)

break value is valid for loop. There's no other possible value for a loop expression, but a for/while loops can terminate normally, so it doesn't make sense to break value there.

To be clear though, what I was talking about is

'outer: loop {
    'inner: loop {
        break 'outer;
    }
}

where break 'label escapes both loops. This can be combined, e.g. break 'label value.

Python multi-level break and continue

Posted Sep 4, 2022 22:59 UTC (Sun) by tialaramex (subscriber, #21167) [Link]

The interesting unexpected thing I found while researching details of that previous comment was that Rust actually explicitly forbids "break value;" syntax in for and while loops. I had imagined that it was permitted but, because those loops have the empty tuple as their type the only possible value I can break with would be the empty tuple and so I'd get a type error if I tried anything else. But no, I tried it and you can't do this even with the empty tuple as value.

Python multi-level break and continue

Posted Sep 1, 2022 14:42 UTC (Thu) by flussence (guest, #85566) [Link] (9 responses)

Where Perl's label construct differs from a C goto is that Perl's can only jump laterally or upwards; a C goto can jump deeper into the call stack or to unrelated code, with all the headaches that can cause. This version of goto/continue/break is more like throwing and catching an exception - in Raku they're actually implemented as a special case of those.

I think it'd be a good solution to Python's problem. I've used numbered breaks in PHP code and can't say it was much good for maintainability.

Python multi-level break and continue

Posted Sep 1, 2022 16:31 UTC (Thu) by rgmoore (✭ supporter ✭, #75) [Link] (1 responses)

Perl's next and last constructs only work with loop labels, but it does have a goto statement that can jump to just about anywhere you tell it. Loop labels are one of those things people use when they feel they have to, but I don't think I've ever seen anyone use an actual goto.

Python multi-level break and continue

Posted Sep 2, 2022 6:54 UTC (Fri) by anselm (subscriber, #2796) [Link]

Perl has goto mostly because it made the Awk-to-Perl translator easier to implement. It was never recommended for general usage by people in their own programs.

Python multi-level break and continue

Posted Sep 1, 2022 20:39 UTC (Thu) by bartoc (guest, #124262) [Link] (6 responses)

C goto can not jump deeper (or shallower) in the callstack. It's a local goto.

It can jump "down" through a function though which sounds scary but in reality it's almost always used for jumping to cleanup code.

C goto is not the goto that Dijkstra was railing against, and is basically fine.

This whole discussion is honestly one of my biggest annoyances with the python community, it's pretty clear a lot of the code examples were being compared on just "personal preference", not any real concept of maintainability.

Python multi-level break and continue

Posted Sep 1, 2022 23:12 UTC (Thu) by tialaramex (subscriber, #21167) [Link] (5 responses)

Right, Dijkstra's ketter is about the go-to feature as manifested at that time which is way more powerful (and thus dangerous) than anything mainstream languages support today. Dijkstra is arguing for what we have today, loops, procedure calls, structured control flow rather than just jumping around.

For the cleanup problem, I think C successors tend to offer defer here for that purpose. I don't like defer, but it's a more healthy solution than goto fail: type arrangements. I prefer something like Rust's Drop trait but I can see that feels too much like invisible magic for some C practitioners.

Python multi-level break and continue

Posted Sep 1, 2022 23:35 UTC (Thu) by NYKevin (subscriber, #129325) [Link] (1 responses)

Strictly speaking, you can recover the goto that Dijkstra was talking about very easily in C. You just have to put all of your code in main, and then C's goto is functionally identical to the original goto.

> For the cleanup problem, I think C successors tend to offer defer here for that purpose. I don't like defer, but it's a more healthy solution than goto fail: type arrangements. I prefer something like Rust's Drop trait but I can see that feels too much like invisible magic for some C practitioners.

People have come up with many, many different solutions to the cleanup problem:

* try-with-resources (Java)
* with/using statements (Python/C#)
* defer (Go)
* Destructors (C++, Rust)
* finally (just about any language that supports exceptions, including several of the above)
* Higher-order functions like Haskell's withFile
* Optional types (which allow you to coalesce the happy path with the unhappy path so that you only have one code path and don't need to "handle" cleanup specially)

Most of these options would require C to grow something vaguely resembling nontrivial data types (what C++ would call non-POD types), which will obviously never happen, but defer would make logical sense for C, IMHO. You could also make an argument for finally, because even without exceptions, it still intercepts early returns (as well as more convoluted nonsense like break and continue, and one imagines it could also do goto).

Python multi-level break and continue

Posted Sep 1, 2022 23:36 UTC (Thu) by NYKevin (subscriber, #129325) [Link]

> You just have to put all of your code in main, and then C's goto is functionally identical to the original goto.

For people with no sense of humor: This is a joke. Do not actually do this.

Python multi-level break and continue

Posted Sep 1, 2022 23:42 UTC (Thu) by bartoc (guest, #124262) [Link] (1 responses)

Well, you can get something much like rust's drop trait in gnu C (supported in clang as well) using __attribute__((cleanup)) and some macros. This actually ends up working really quite well and makes things much more ergonomic.

In any event I actually want all of goto, defer and drop/RAII. Defer is useful even with drop/RAII when you are interfacing with external code or doing something "one off". Goto remains useful for implementing certain algorithms involving state machines and tables.

While not super useful in combination with goto there is a related gcc extension called "labels as values" that I really wish would be standardized, since it allows writing code that would be almost impossible otherwise (like tracepoints).

Python multi-level break and continue

Posted Sep 2, 2022 11:08 UTC (Fri) by tialaramex (subscriber, #21167) [Link]

While it's not something you'd put in a slim C-like language, if you have say, closures, then drop replaces the need for the defer feature. You can just build a utility type whose purpose is to run a closure when it is dropped, so then when you make the utility type and specify a closure, it will run that closure when the scope ends, the exact same feature as defer but now just a utility type.

There are (of course) several of these in Rust crates already, suiting different tastes but it's also trivial to build your own instead.

And for state transitions I'd definitely again rather have high level types representing this than try to do it with goto. Pattern matching and sum types make this sort of thing feel very ergonomic to me compared with C. In fact while I understand a C successor isn't going to have Rust's quite sophisticated ControlFlow type, it'd be nice to at least have say <control.h> defining CONTINUE and BREAK so that Library A's maybe_do_stuff() and Library B's repeat_until_done() both agree that CONTINUE is 1 (or 4, or 'Z' I don't care) while BREAK is 0 (or -26, or 5.16, again I don't care) rather than have a situation where Library A signals with -1 while Library B expects 0

Python multi-level break and continue

Posted Sep 7, 2022 15:39 UTC (Wed) by anton (subscriber, #25547) [Link]

I don't find anything in "Go to Statement Considered Harmful" that suggests that Dijkstra meant only variants of goto that are more powerful than those in C.

Python multi-level break and continue

Posted Nov 29, 2023 16:16 UTC (Wed) by blommaep (guest, #168263) [Link] (1 responses)

The labeled loop is not only safe, it is also the most clean construct for such cases (nested loops with 'short' body).
It's easy to write: you never need to doubt where the break/continue will jump, no special constructions
It's easy to read: just find the label
I really fail to understand why it only exists in perl

Python multi-level break and continue

Posted Dec 1, 2023 4:04 UTC (Fri) by mathstuf (subscriber, #69389) [Link]

Python multi-level break and continue

Posted Sep 1, 2022 2:28 UTC (Thu) by gutschke (subscriber, #27910) [Link]

In languages that support this type of construct, I have on occasion found it useful. And in fact, even when programming in plain C, this is the only type of goto statement that I am fine with in my own code.

Doesn't happen often, but every few years it's convenient. If memory serves correctly, in the vast majority of cases it was useful in order to jump out of all enclosing loops. In other words, I mostly used labeled breaks (or the equivalent goto statement) but can't remember the last time that I used labelled continues.

Importantly, in situations when I did use this feature, I didn't want to jump out of the entire function, because I still needed to do some final clean up or error handling. So, refactoring into a separate function wasn't always appropriate.

Of course, in Python, that's easily addressed by taking advantage of try/finally. The only remaining issue with this approach has to do with scoping and the lifetime of variables when refactoring into separate functions. But inner functions mostly address that concern. So, I agree that maybe in Python, this need for labelled breaks and continues just doesn't arise all that often.

Having said that, I can absolutely envision scenarios where a labeled break would be slightly more readable than a combination of inner functions and try/finally statements. Whether that's good enough to justify a new language feature is a different question.

Python multi-level break and continue

Posted Sep 1, 2022 8:57 UTC (Thu) by NRArnot (subscriber, #3033) [Link] (2 responses)

I've wanted to break out of nested loops a fair number of times. I've always found its fairly easy to work around. Sometimes raise and catch an exception, sometimes test a flag variable in the outer loop(s). Raise-and-catch has the added advantage that it can unwind function calls (and if it's a large loop structure, you may well want to re-factor the inner body into one or more functions or methods).

That said, I like the labelled-loop suggestion.

Python multi-level break and continue

Posted Sep 1, 2022 11:33 UTC (Thu) by caliloo (subscriber, #50055) [Link]

I have used the test variable and break method. But I must admit that every time, I did it in quick and dirty scripts a few 100 lines, that I did not want one minute more than necessary in.
It would be a useful tool for quick and dirty mods on top of existing things. But I think it would not get old in a code base really well. (I’m thinking interlaced label and breaks, now go read your code).

I find that one of the values of python is that it simply does not let you do too much dumb stuff language wise, no matter how in a hurry and clever you are… So for that reason I think I can live without it.

Python multi-level break and continue

Posted Sep 1, 2022 12:32 UTC (Thu) by Wol (subscriber, #4433) [Link]

I believe DataBASIC has the labeled loop approach ... (but not the version I saw here)

FOR I = 1 TO 10
...
CONTINUE I_WANT_OUT:
...
I_WANT_OUT: NEXT

It's a syntax error if the continue refers to a line-label that is not a valid loop terminator. Makes it dead easy to see where you are jumping to.

Cheers,
Wol

Exceptions

Posted Sep 1, 2022 9:28 UTC (Thu) by pwfxq (subscriber, #84695) [Link] (2 responses)

A couple of comments have suggested using exceptions to break out of inner loops.

1 - Aren't exceptions supposed to be for "exceptional" circumstances/issues?
2 - Unless you create a nest of exceptions, how do you control which level loop you're breaking back to?

Exceptions

Posted Sep 1, 2022 11:49 UTC (Thu) by petrm (subscriber, #126046) [Link]

You would think so, but Python has a built-in exception to signal that an iterator has nothing else to yield: https://docs.python.org/3/library/exceptions.html#StopIte...
Seems to me that the fact that an iterator has been fully drained is not exactly an unexpected event. That's bound to happen at some point. And yet, exception. (Because sure, how else are you going to signal this?) Using exceptions for multi-level loop exit seems rather similar to this use case.

Exceptions

Posted Sep 1, 2022 16:30 UTC (Thu) by k8to (guest, #15413) [Link]

The reasons to avoid exceptions don't really exist in python. They arent dangerous or slower than other options. They're just anothe control flow that work well cor problems but are fine for other bailout scenarios.

They would be awkward if you had a need to jump out to a variety of levels but i have difficulty imagining a usecase for that.

Python multi-level break and continue

Posted Sep 1, 2022 10:44 UTC (Thu) by alfille (subscriber, #1631) [Link]

I found the easiest approach is a "return" (putting the inner loops in a subroutine).

I know this has performance issues and perhaps variable scoping issues, but it also can add code clarity.

Python multi-level break and continue

Posted Sep 1, 2022 12:01 UTC (Thu) by jezuch (subscriber, #52988) [Link] (3 responses)

> Millionaire thought that requiring refactoring into a function was less than ideal.

In my 20+ years of software engineering experience I find this to be the optimal solution to the "problem". Splitting code into smaller functions is generally considered a Good Practice.

Labeled break seems like a useful(*) feature, but it's almost always an indication that the code is due for some refactoring. And it's a huge red flag sure to be drawing unwanted attention of predators (code reviewers) :)

(*) it also feels "neat". I suppose that's 80% of the reason our geek brains want it.

Python multi-level break and continue

Posted Sep 1, 2022 12:42 UTC (Thu) by eru (subscriber, #2753) [Link] (1 responses)

Splitting code into smaller functions is often good practice, but not always. I often find it clearer to have the whole inline, so it can be seen at a glance, instead of lots of small functions used only once. The latter is like reading a text that keeps jumping into footnotes!

As a contribution to the fun syntax bikeshedding, a language I was involved in implementing had a loop roughly like this:

while cond1
    while cond2
    ...
    // Want to break out of both loop levels?
    if satisfied
        break foundIt
   endwhile
endwhile foundIt
 
The break is a jump forward, so it makes sense the label is at the endwhile. Of course this wont work too well in Python that lacks a delimiter to mark the end of the loop, other than decreasing indentation.

Python multi-level break and continue

Posted Sep 1, 2022 21:00 UTC (Thu) by bartoc (guest, #124262) [Link]

Strongly agree with this take. If you aren't going to reuse it then it's usually not worth splitting stuff out it's own function. Just let me read the darn code. If the function is doing some well known algorithm maybe it _is_ worth it, but even then sometimes a comment is just as good as a function.

Keeping things inline can make optimization opportunities more clear and make anti-optimizations stick out more, as well.

Python multi-level break and continue

Posted Sep 1, 2022 23:48 UTC (Thu) by excors (subscriber, #95769) [Link]

I find splitting into smaller functions (just so you can replace the multi-level break with a return) is the optimal solution in maybe 95% of cases. But in the other 5% it can be very awkward, usually because there are several variables shared between the outermost and innermost code and you'd have to explicitly pass them all across the new function boundary (particularly annoying if you've given them nice long descriptive names, which you now have to duplicate into the function definition and the function call), or (even more rarely) because you've already split it once and if you add another function layer then 'return' won't return far enough.

In those cases the code can be simpler and more clearly express the programmer's intent by using a multi-level break instead. They're not very common cases and the workarounds aren't too bad, so it's not a crucial language feature (as demonstrated by the successful languages without it), but it's also not a feature that seems to be widely abused or that causes great confusion (as demonstrated by the successful languages with it), so I think on balance it's still nicer if the language gives you the option.

Python multi-level break and continue

Posted Sep 1, 2022 13:32 UTC (Thu) by mathstuf (subscriber, #69389) [Link]

I've been thinking about when I've seen (or done) it. It's usually in C or C++ where I want to break from a loop from within a `switch`. `continue` works, but `break` is unfortunately overloaded. So much would have been better if fallthrough were not an implicit thing there… But Python doesn't have that problem, so I don't think there's much value here in it for Python.

Python multi-level break and continue

Posted Sep 1, 2022 15:53 UTC (Thu) by JoeBuck (subscriber, #2330) [Link] (4 responses)

Multi-level break/continue with a loop label is part of Ada, VHDL, and Perl; all three spell 'continue' as 'next'; Ada and VHDL say 'exit' instead of 'break' and Perl says 'last'. Verilog has 'disable' which can be used to jump out of any labeled statement (which might be a loop or a begin/end block). But all of these rely on attaching a label to the region of code in question, so it's stable as the code is maintained (and perhaps extra loops are added).

But I can't think of any language that uses a number of loops to jump out of as an argument to break/continue or next/exit.

So if it's done I think the label approach should be used.

Python multi-level break and continue

Posted Sep 1, 2022 16:35 UTC (Thu) by k8to (guest, #15413) [Link] (1 responses)

TCL does the break 2 thing. Unfortunately.

Python multi-level break and continue

Posted Sep 3, 2022 2:35 UTC (Sat) by dskoll (subscriber, #1630) [Link]

If by TCL you mean Ousterhout's Tcl, nope. break does not take an argument and nor does continue. Supplying one gives an error, at least in Tcl 8.6.11.

Python multi-level break and continue

Posted Sep 4, 2022 23:09 UTC (Sun) by intelfx (subscriber, #130118) [Link] (1 responses)

> But I can't think of any language that uses a number of loops to jump out of as an argument to break/continue or next/exit.

Bash?

```
break [n]
Exit from within a for, while, until, or select loop. If n is specified, break n levels. n must be ≥ 1. If n is greater than the number of enclosing loops, all enclosing loops are exited. The return value is 0 unless n is not greater than or equal to 1.
```

Python multi-level break and continue

Posted Sep 7, 2022 18:28 UTC (Wed) by jwilk (subscriber, #63328) [Link]

Not only bash; "break N" is in POSIX:
https://pubs.opengroup.org/onlinepubs/9699919799/utilitie...

Python multi-level break and continue

Posted Sep 1, 2022 18:33 UTC (Thu) by edeloget (subscriber, #88392) [Link]

Ouch. My head hurts.

Frankly, in the name of readability, this looks like a very good candidate for modularisation. Your inner loop is exiled in a function that returns a specific value when it "break", and this function is called by the outer loop (which can then break if it receive said specific value when it calls it).

This has many other advantages, such as:

* not melting the mind of people who are reading your code ;
* open yourself to reuse of the inner loop
* not needing to change the whole language to support a feature that will only make you sad and angry in the long run.

And if you are using a compiled language, there is a good chance that it will be able to optimize something (and if you're not using a compiled language then the additional time it would take to evaluate an additional function is not really an issue).

Python multi-level break and continue

Posted Sep 1, 2022 20:44 UTC (Thu) by bartoc (guest, #124262) [Link] (2 responses)

The complaint about "trying to do ad-hoc relational querying with imperative code" is particularly frustrating because that's a great example of an algorithm that this stuff makes easier! There's nothing wrong with writing out your "relational queries" instead of going through the whole query planner/optimizer/code generator of a database.

Python multi-level break and continue

Posted Sep 1, 2022 21:41 UTC (Thu) by Wol (subscriber, #4433) [Link]

> the whole query planner/optimizer/code generator of a database.

of a *RELATIONAL* database.

Pick does not have a query planner - the optimal plan is "do as you're told".

Pick does not have an optimiser - okay, it's down to the user to make sure all your indices are properly declared, but in a well-designed database there is nothing to optimise.

Pick does have a compiler, but the object code lives in the database and is compiled at development time, not run time.

And the same is true for most other non-relational databases I would think. Hierarchical databases are mostly drill-down and optimised for fast access along common paths. Pick takes that to the next level - it's drill-down and optimised for fast access along any *sensible* path.

Cheers,
Wol

Python multi-level break and continue

Posted Sep 2, 2022 7:23 UTC (Fri) by NYKevin (subscriber, #129325) [Link]

I would not recommend doing relational querying by hand in Python, for three main reasons:

1. Python is slow.
2. Relational querying by hand implies relational querying in a language other than SQL, which is going to be harder to maintain since SQL is the standard language in this space.
3. Python comes with a bundled copy of SQLite, which has neither of the above problems and is ridiculously easy to set up and use.

Python multi-level break and continue

Posted Sep 3, 2022 6:50 UTC (Sat) by amarao (guest, #87073) [Link] (1 responses)

I like break 2 approach. If we allow to use values (`break foo`), it can help to write the most hilarious code golf.

Also, I want multi-level return, which forces outer functions immediate return. What a fut it would be!

Python multi-level break and continue

Posted Sep 7, 2022 19:27 UTC (Wed) by Wol (subscriber, #4433) [Link]

:-)

DataBasic has, I believe, RETURN TO ... , which is probably what you're thinking of ...

I don't believe it is ever used in normal code! Certainly I've never seen it, let alone used it ...

Cheers,
Wol

Python multi-level break and continue

Posted Sep 3, 2022 18:19 UTC (Sat) by Shugyousha (subscriber, #93672) [Link]

Golang has labeled "break" and "continue" statements: https://medium.com/golangspec/labels-in-go-4ffd81932339

I don't think I have seen it in production code more than once or twice in ten years of writing Go code. It looked like a code smell then as well ...

Python multi-level break and continue

Posted Sep 3, 2022 22:46 UTC (Sat) by neilbrown (subscriber, #359) [Link] (1 responses)

I would suggest that "break" and "continue" are the problem rather than the solution, and they should be deprecated. That would, of course, require a somewhat richer looping/conditional construct. I quite like the "Event indicators" described in https://pic.plover.com/knuth-GOTO.pdf (which is well worth reading in its entirity if you haven't already). They are only part of a solution, not a complete design, but I think they point in a good direction.

For a python-like language, I think a flag is the right approach, and if it appears clunky, then that is the fault of the language, and that is where we should look for a fix.

With a c-like for loop, it might appear

   for (piter = all_players.iter(); player = piter.next() ;)
      for (ptiter, abort = all_tables.iter(), False; not abort and player_tables = ptiter.next();)
         for (viter = player_tables.iter(); not abort and version = viter.next();)
              # things  have gone wrong, need to break
		abort = True

	    this_is_not_reached = True

	this_line_is_called()
This is clunky, but not because of the abort (I think) but because you are forced to split the 'init' and 'next' steps of walking the table. Python combines these (good) but doesn't let anything be added (bad).

Maybe a "while" clause in the for loop?

    for player in all_player:
        abort = False
	for player_tables in all_tables while not abort:
	    for version in player_tables while not abort:
		# things have gone wrong, need to break
		abort = True

	    this_is_not_reached = True

	this_line_is_called()
This makes it obvious from the start which loops might abort early. I think it is good to be obvious. With break, it might not be obvious until the end of the loop, which isn't good for readability (unless you keep all loops to 5 lines).
You still have a clunkiness that 'abort' must be initialised before the loop. Maybe this is good as it become trivial to know if the loop aborted.
(of course a good compiler would optimise the flag away and use gotos.)

Python multi-level break and continue

Posted Sep 5, 2022 0:48 UTC (Mon) by NYKevin (subscriber, #129325) [Link]

> Maybe a "while" clause in the for loop?
>
> [...]

I'm having a hard time seeing how that's supposed to be more readable than just writing break like we do now. Besides, if you really want while-like syntax, you can already write this:

piter = iter(all_player)
while player := next(piter) and not abort:
    # etc.

I just don't see why we need to bolt on an extra clause to the for loop, given that you can just use break instead, or rewrite as a while loop. There are already two ways of doing it, why should we add a third?

Python multi-level break and continue

Posted Sep 8, 2022 11:02 UTC (Thu) by dboddie (guest, #55461) [Link]

Limbo (https://www.vitanuova.com/inferno/papers/limbo.html) also has labelled breaks and continues, though maybe the use case for them in that language is stronger given that break is needed to escape from case (switch) structures. However, it seems that the feature is still useful for nested loops.

In any case, use of labels can help to make the intent clearer in the code, making it explicit which loop is being escaped or continued. In Python you would use a comment for that, perhaps, but you would need to be careful to update it if the code was refactored.

Python multi-level break and continue

Posted Sep 8, 2022 15:31 UTC (Thu) by wvaske (subscriber, #114127) [Link] (4 responses)

I really like the idea of context manager style for loops:
for team in conference as conference_loop:
    for player in team as team_loop:
        if condition:
            conference_loop.break()
        else:
            team_loop.continue()
It's pythonic and makes break and continue much more explicit. Even in normal code without a double break, knowing which iterator a break or continue belongs to would be helpful.

That would take some python internals updating but we could define our own for_loop context manager:
with for_loop(team, conference) as conference_loop:
    with for_loop(player, team) as team_loop:
        if condition:
            conference_loop.break()
        else:
            team_loop.continue()

Python multi-level break and continue

Posted Sep 8, 2022 16:10 UTC (Thu) by mathstuf (subscriber, #69389) [Link] (3 responses)

What happens with this?
def control_flow_really_should_be_local(loop):
    loop.break()

def i_mean_how_is_this_supposed_to_work(loop):
    loop.continue()

with for_loop(team, conference) as conference_loop:
    with for_loop(player, team) as team_loop:
        if condition:
            control_flow_really_should_be_local(conference_loop)
        else:
            i_mean_how_is_this_supposed_to_work(team_loop)

Python multi-level break and continue

Posted Sep 9, 2022 4:56 UTC (Fri) by foom (subscriber, #14868) [Link] (2 responses)

Not that this seems like a good idea but...

A context manager can already handle and optionally catch and suppress exceptions in its __exit__ handler. It cannot, currently, restart execution at the beginning of the 'with' region from __exit__, but that is an easy to imagine addition, which might even be useful for other things, e.g. "with retries(3): do_something_that_might_fail()"

Once you have that simple change, a for_loop context manager is basically implementable. The .break() and .continue() function calls simply throw a special exception type, which the exit handler swallows -- and then either restarts the with region or doesn't. Upon normal exit, the exit handler also determines whether to restart the region based on the loop condition.

Done.

Python multi-level break and continue

Posted Sep 9, 2022 6:42 UTC (Fri) by dtlin (subscriber, #36537) [Link] (1 responses)

Hypothetically, if __enter__ could be a generator, with the with body running for each yielded value, that would give us the power to implement
class Loop:
    def __enter__(self):
        self._do_continue = True
        while self._do_continue:
            self._token = object()
            yield LoopHandle(self._token)

    def __exit__(self, type, value, traceback):
        if type is LoopControlException and value._token is self._token:
            self._do_continue = value._do_continue
            return True

class LoopHandle:
    def __init__(self, token):
        self._token = token

    def break_(self):
        raise LoopControlException(False, self._token)

    def continue_(self):
        raise LoopControlException(True, self._token)

class LoopControlException(Exception):
    def __init__(self, do_continue, token):
        super().__init__()
        self._do_continue = do_continue
        self._token = token

with Loop() as outer:
    with Loop() as inner:
        outer.break_()
I'm undecided whether this is a good thing or not, but it was surprising to me how relatively little it would take to get there.

Python multi-level break and continue

Posted Sep 19, 2022 12:04 UTC (Mon) by sammythesnake (guest, #17693) [Link]

I actually really like this - to my mind it answers all the qualms I've had and AFAICS those others have expressed (based on the article/comments here - I've not read the threads)

It's easy to read, nicely encourages clear naming/labeling (at least in a much as a syntactic requirement to *have* a label), requires no extra charges to the language...

The only niggle is that the syntax of having a method call serve the purpose of a flow control construct is a *little" klunky...

It's even the kind of thing that might make a good library/module, even if an unusually small one :-P - perhaps a candidate to be included in an already existing standard library module (¿itertools?)

Some thoughts on how it could be expanded - more hands-holding support for more styles of loops, such as specific buttons for while/until/for/whatever. Perhaps parameters to the init of the Loop class would be the place to do that: "with Loop(while=<expr>): ..." or something.

Perhaps a future possibility would be syntactic support to adding this kind of context manager to other loops (like somebody above suggested using the "with" keyword) though that might end up with duplicate functionality if the context manager has already implemented features for specific loop like concepts, so that might be something to decide on sooner rather than later...

I'd also use a base class and child classes for the exception because a Boolean doesn't give much scope for distinguishing varieties of flow control conditions and there might be further future additions here (Possibly a retry the current loop option, for example...?)


Copyright © 2022, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds