Python has an odd tolerance for try - except controlling the flow. They have also an ancronym to justify it: EAFP, which is Easier to ask for forgiveness than permission.
Surely it is often easier, but easier doesn't mean necessarily good, or better 〈positive adjective you prefer〉.
Now, despite what I've just written, I am fine with it — except when indeed I am not, and it happens especially because flocks of users feel allowed to use exceptions without any grain of wisdom.
Boring languages instead teach this: do not use exceptions to control the flow. Which is perfectly logical given the name exception.
If Python idiom really allows overusing exceptions, maybe they should have called them differently. For example, events, and instead of
except they should have
occur), or anything but
StackOverflow's links on the subject:
- Using try vs if in python
- Is it a good practice to use try-except-else in Python?
- Cost of exception handlers in Python
- Are exceptions for flow control best practice in Python?
- Python using exceptions for control flow considered bad?
Thoughts on “controlling the flow”
In Python, throwing exceptions for normal situations, and catching exceptions rather than preventing them, is more idiomatic than in other languages. For instance, an iterator's next method throws an exception to indicate when all elements have been visited.
NotFound is an example which doesn't bother me. I am inside an interator, the caller is asking for more, and I raise the flag: hey, no more for you. The exception goes up to the caller which then will know there are no more elements. And since the caller is iterating over collection or whatever, this
NotFound happens once after having run over N elements, then it is an exception, after all.
This can be “controlling the flow”, but I would rather classify it as signaling the caller about a situation.
When I think about “controlling the flow”, I am thinking more like this:
x = -10 try: if x < 0: raise ValueError print("positive") except: print("negative")
Ok, this is extreme and nobody does it — but it must be pythonic, maybe just a little bit too much. A more serious example, taken from here, slightly modified.
alist =  with open('e.txt') as e: for line in e: s = line.split() start = int(s) target = int(s) try: if s == 'nope': continue except IndexError: alist.append([start, target])
Pythonic brain at work: try to access the index of an element you know it could not exist, and in case it doesn't exist, … forgive me, but this is the good case when you want to store
What does it matter is that there's a try-except while reading from a file… how many lines have only two elements, and how many lines have three or more?
The try-except can be replaced by a simple if-else, which is at least as much as readable as the try-except.
if len(s) > 2 and s == 'nope': continue else: alist.append([start, target])
This is slightly different, though: if
s exists and it isn't
target are appended, and the same happens when
s doesn't exist.
When there are less than 2 elements, and when
int(...) fails, exceptions are raised, and this is good because missing required elements, and non-integer elements, are more exceptional events than the difference allowed by the format of the file.
Even better, we can remove the
if len(s) <= 2 or s != 'nope': alist.append([start, target])
Python idiom may allow to control the flow with exceptions, but doing so here (and in many other similar situations) would be just wasteful nonsense. Saying that Python can use exceptions to control the flow doesn't mean you must do so everytime.
So, Python is not pushing its users to think about it.
I'd go with this simple rule: you won't use exceptions, except for exceptional events and/or when a callee is giving back control to a caller with a special condition (like
NotFound on a
Pythonistas aren't Haskellers (these have their Church), nonetheless somebody downvoted me because I haven't aknowledged that Python has its path through control flow, and that path accepts exceptions… Except when it doesn't, i.e., everywhere it doesn't make sense, like here and in many other situations, again.
All not lazy
A solution looked like this:
if all([len(words) > 2, words == 'nope']): continue else: alist.append([int(words), int(words)])
I don't think it is an obscure way of writing the
A and B condition, but anyway it doesn't work because first the list is constructed — i.e., all the values are evaluated — then
all() iterates over those computed values.
This is unfortunate, because it means that
words is evaluated, no matter if the result of
len(words) > 2 is true or false. The
all() short-circuits, but isn't lazy.2
Let's take a look at the implementation of all, and let's roll our “lazy” version:
def lazy_all(iterable): for el in iterable: if not el(): return False return True
if lazy_all([lambda: len(words) > 2, lambda: words == 'nope']): continue
works, but I agree that this is worthless. Isn't it?
This seems buggy because when
sexists and it isn't
nope, nothing happens anyway — but maybe it is what he wanted. Here it doesn't matter.↩
In fact it is just a function receiving an “array”, so the common order of evaluation is used. To have lazyness, it should have been a special syntax to handle in a special way, evaluating B only if the evaluation of A gave true.↩