1. 43
    Or else python orbifold.xyz
  1.  

  2. 17

    I’ve been writing Python for years and had absolutely no idea else could do this. Massive thanks for this short, sweet article.

    1. 11

      Very simple syntax, but I’m not sure I like these constructs. Unless I remember the specification, my knee-jerk reaction is to be surprised and suspicious upon seeing that kind of code. Is it going to be executed or not? Is it a merge conflict resolution gone bad? Especially if there are neighbouring if/else blocks with almost correct indentation.

      Although, I must admit the for/else variant is great for simplifying searches in iterables :)

      1. 6

        This is one of those cases where I think the Python solution is elegant but impractical. I find it much harder to reason about (and consequently I strongly suspect it is more error prone) than (for example) the Go solution to the same problem, which is to continue LABEL (example). That said, I recently screwed up some Go code by typing goto instead of continue, thereby creating a surprising infinite loop.

        1. 3

          I predict that your knee-jerk surprise and suspicion will give way to mild feelings of approval as you get more comfortable with Python idioms. Other languages I’ve known have much stranger idioms, which also become ‘intuitive’ with familiarity. Language is a wonderful thing.

          1. 2

            Is it an idiom (as in, commonly used pattern)? I’ve known about this for years, but never used or seen it in any code.

          2. 2

            In my opinion, the most basic requirement of someone working on Python code is to know Python’s syntax. And in fact that goes for any programming language. One of the best things about Python is that it has really simple syntax for a language of its type (nothing approaching Lisp’s simplicity). It requires only a single token of lookahead to parse, which I think helps humans and computers. There are no “garden path” syntactic constructs where you only realise half way along a line that you need to go back to the beginning and start parsing that line again, like you can get in C++:

            x * y = z;
            

            So when you say ‘Unless I remember the specification’, it makes me wonder whether you actually know the language properly or whether you’re just cargo-culting code based on some sort of surface-level knowledge of the language that you’ve gained only by reading a lot of code. Python’s grammar is right there in the documentation, all these constructs are well-documented and I remember them being covered in the first Python book I ever read, so there’s really no excuse not to be familiar with them.

            1. 1

              I know a lot of people aren’t really fond of Django’s template language, but its for tag uses an optional empty block instead of an else block to clarify that it’s only for things you want to do when the thing you tried to iterate was empty.

              1. 4

                …which is not the same as what for/else does.

                1. 1

                  While it’s true that Python’s for/else is not quite identical to the Django template language’s for/empty, handling an empty iterable is the primary use case I’ve seen in the real world for people using Python’s for/else, and avoids some of the confusion of what Python’s for/else actually does (which is, I’d argue, a poor fit for else).

                2. 1

                  A lot of template engines support this. Actually, I think pretty much all of the ones I’ve worked with over the years across a variety of languages.

              2. 7

                Don’t forget the ternary expression form of if/else: <e1> if <condition> else <e2>. Very concise and handy.

                1. 2

                  I was only on a python project for about a year, but this one always tripped me up for some reason. My brain just couldn’t handle the condition being in the middle, sandwiched between the outcomes.

                2. 7

                  I had a lot of trouble understanding the description of try/except/else. For anyone else who is confused, it works like this:

                  try:
                      # Run this first. Exceptions will be caught by `except`.
                  except ValueError:
                      # Jump to this if the `try` block threw an exception.
                  else:
                      # Run this after the `try` block, only if the `try` block didn't throw an exception.
                      # Exceptions thrown here will *not* be caught.
                  
                  • How is the else block different from just writing code after the try/except construct?
                    • The else block will only run if an exception was not thrown in the try block.
                  • How is the else block different from writing code at the end of the try block?
                    • Exceptions in it will not be caught by the except block. That is, exceptions in it are unexpected and shouldn’t be hidden.
                  1. 3

                    else does not run on loop termination. It only runs if the loop condition was never true in the first place, ergo if there is no loop execution at all.

                    Also, I believe that fundamental syntax tutorials do not belong on Lobsters. Short-form content like this would be better-suited to, e.g., a Twitter thread. But I’m willing to hear counterpoints.

                    1. 10

                      I don’t think I agree with syntax tutorials not belonging on Lobsters. I certainly wouldn’t want them to be the majority of the content, but a) this is a feature pretty unique to python that isn’t super well known and b) most new programming language projects include a nonzero amount of fundamental syntax tutorials.

                      1. 2

                        b) most new programming language projects include a nonzero amount of fundamental syntax tutorials.

                        And that’s exactly it: in my mind, Lobsters is for new or novel things. If a story is old, ideally it’s something that’s being uncovered and that most people haven’t seen. Maybe this story occupies a nice little niche of things most people have seen, yet many don’t fully understand.

                        I mainly recall the barrage of Ruby tutorials that came a while back, muddling the front page and turning me off to these sorts of short one-off articles. But I checked the author’s archive and it seems they’ve got plenty of interesting stuff under their belt. I really look forward to seeing more of that sort!

                      2. 7

                        else does not run on loop termination. It only runs if the loop condition was never true in the first place, ergo if there is no loop execution at all.

                        That’s not correct. If numbers is [0,2,4,6,8] it will print “No odd found”. The else always runs if break is not hit, regardless of what’s in the iterable being iterated.

                        1. 6

                          Unless I misunderstand you, it looks like what you’re saying is exactly wrong. For example:

                          ❯ cat /tmp/test.py && python3 /tmp/test.py
                          def print_first_odd(numbers):
                              for x in numbers:
                                  if x % 2 != 0:
                                      print(f'Found odd number: {x}')
                                      break
                              else:
                                  print(f'No odd numbers in [{",".join(map(str, numbers))}]')
                          
                          
                          print('---------------------')
                          print_first_odd([1,2,3,4,5])
                          print_first_odd([2,4,6,8,10])
                          print_first_odd([])
                          print_first_odd(None)
                          
                          ---------------------
                          Found odd number: 1
                          No odd numbers in [2,4,6,8,10]
                          No odd numbers in []
                          Traceback (most recent call last):
                            File "/tmp/test.py", line 14, in <module>
                              print_first_odd(None)
                            File "/tmp/test.py", line 2, in print_first_odd
                              for x in numbers:
                          TypeError: 'NoneType' object is not iterable
                          

                          I’ve been using python for non-trivial things since 2001 and I didn’t know else worked this way in python until today. I was happy to see this linked on lobste.rs and would have found it harder to read on twitter than on this easy-for-me-to-read blog post.

                          And I would also posit that the fact that you didn’t understand the syntax and I didn’t know about it after using python (not every day but plenty) for this long would make this something other than a “fundamental syntax tutorial”.

                          1. 5

                            Looks like I can’t edit the original comment for corrections, but I now understand the distinction with respect to for/else. Thanks for the insightful replies, I guess maybe I learned something from this story after all!

                            Also, maybe the number of people agreeing with my incorrect statement indicates how poorly understood this feature is. Perhaps a reason to use it sparingly…

                            1. 3

                              Your incorrect statement forced me to try the feature out and understand it. (At least in the context of for…else) So thanks for posting it.

                              After letting the construct bounce around my brain for a few hours and looking at a couple of places I could have used it, I have two more thoughts:

                              1. I kind of like the idea, especially when it comes to eliminating sentinel values. I don’t think this is just an affinity for code golf, either. Sentinels add their own form of ugly noise, and I’ve certainly spent time in the past chasing bugs that this construct would’ve very likely prevented me from writing.

                              2. I really hate that they reused else for it. Coming up with a different name, or even reusing break with a colon after it would’ve been better IMO. I think that’s mostly because

                              for foo in bar:
                                  if baz:
                                      do_bip()
                              

                              is such a very common construct. My eyes have a hard time telling whether an else is indented to match the if or to match the for and the difference between the two is quite load bearing here. In a curly brace language, it might be less challenging to read.

                              So I’m finding myself inclined to use it very sparingly after thinking about it. I suspect there are several places where killing sentinel values will be worth it as long as it’s accompanied by a clear comment, though, even if the readability factor prevents me from using it anywhere else.

                              1. 2

                                Personally, I guess the rationale comes from the try: construct and not the if: meaning of the word. The latter is of course the one that most people think of first, which also happened for me.

                                From the meaning of the keyword I would have preferred finally, but that would clash with how it is interpreted in a try block.

                                1. 1

                                  Personally, I guess the rationale comes from the try: construct and not the if: meaning of the word. The latter is of course the one that most people think of first, which also happened for me.

                                  The rationale for for/else is that for is just sugar for while. The rationale for while/else is that while is just sugar for if condition: jump backwards and then the else is just attached to that if.

                                2. 1

                                  I suppose other languages tend to replace “else” with “finally” in some of these. I agree that for…else sounds more like “either loop or do this”, rather than “loop and then do this”.

                                  On the other hand, while good keywords are, well good, more reserved (common) words are bad. I’m not sure if I would really prefer, say for..then or try..then.

                                  1. 1

                                    I appreciate what you said about sentinels. I think an aspect of what’s gross about them is the fact that they’re a state-based solution. Granted, I think any solution would require state in the background, but I think a better semantic solution is in store—and overloading standard for is not it. My best bet is filtering on a condition, then matching the resulting sequence to see whether it’s empty:

                                    def find_odd(numbers):
                                        odds = filter(lambda n: n % 2 == 1, numbers)
                                        try:
                                            odd = next(odds)
                                            print("Found", odd)
                                        except StopIteration:
                                            print("No odd found")
                                    

                                    That way, all the “iteration” (lazy, in this case!) happens before the branching, and the branching itself boils down to a basic if/else construct. I just (ab)used exceptions here because they happened to be a more direct, if perhaps slower, expression of intention.

                                3. 2

                                  I suppose you’re referring to while/else? The for/else example given could be a little more clear if it didn’t contain an if. As is, every x in numbers is indeed tested. If you omit the break, then the else branch is executed at loop end.

                                  1. 2

                                    I had a colleague who used this, else on for loops to determine if the collection was empty - which looked weird, and took a while for me to decipher.

                                    I don’t see why it’s more clear to put an else clause on a loop, instead of testing if the collection is (not) empty before using it.

                                    1. 4

                                      In Python, it is idiomatic to “shoot first and ask questions later.” For example, if you want to read a file, you wouldn’t (or shouldn’t) waste time and code checking to see if the file exists, whether it has the right permissions, is not empty, and so on before trying to read from it. You just try to read from it and then if it can’t be read for whatever reason an exception will be raised and your application decides what (if anything) to do about at that point. This results in code that is easier to read and needs far less boilerplate for common trivial things.

                                      If your colleague was only using the loop to determine if the collection was empty, instead of if not len(collection), yes, that would be very weird. But if it was also used to do some work in the non-empty case, that’s Pythonic.

                                      1. 1

                                        So something like this

                                        for handset in handsets:
                                            <do something with item>
                                        else
                                            raise <custom exception that indicates test failure with extra info>
                                        

                                        Is pythonic or have I misunderstood what you meant, bityard?

                                        1. 2

                                          Actually I went back and read the docs. My point about the Python idiom still stands but we were both wrong about the role of an else clause attached to a for loop. The else clause is executed after the whole list (or whatever) has been iterated over and no break statement was encountered. So you can’t use an else to determine whether the list is empty.

                                          You could check to see whether handset was defined in the else clause to determine if the list was empty (handset will be undefined if handsets is empty and the loop never iterated) but that would be a very roundabout way to avoid len('handsets').

                                          1. 1

                                            Alright, it does make a bit more sense now :-)

                                            But it also confirms my feeling of it being a bit dodgy instead of the len(handsets)