The examples in the article recommend doing the right things, but I think the initial discussion of the LBYL vs EAFP discussion could be better.
The article says basically "LBYL is bad", but this isn't a good description of what the author does in the later examples. Following "EAFP" without exception is also really bad.
The simple policy is that you should use "LBYL" when you're dealing with local state that can't change out from under you, especially when it's just properties of your local variables, but you need to use "EAFP" for anything that deals with remote state. Function calls can go either way, depending.
Blanket advice against LBYL leads people to use try/except blocks to express basic conditionals. If you want to ask something about your local variables, just ask it in a conditional --- don't make the reader infer "ah, these are the situations which will raise the error, and therefore we'll go into this block if the values are this way". If you want to do something when a key is missing from a dictionary, don't write:
try:
value = table[key]
except KeyError:
return default_value
Just write:
if key not in table:
return default_value
value = table[key]
(If you really need to avoid two lookups for efficiency, you would do something like value = table.get(key, MISSING_VALUE) and then check 'if value is missing_value'. But this optimisation will seldom be necessary.)
The example the author gives about interacting with the file system is a good example of where indeed you really should use EAFP. The file system is remote state that your function does not own. Similarly if you're doing database operations, calling a remote API...lots of things.
There's various middle ground when you're calling functions. Often you don't want to worry about whether that function is going to be interacting with remote state, and you want to treat it totally as a black box, so you just use EAFP. But if the function documents really clear pre-conditions of your variables you can just go ahead and check, it's better to do that.
What's really wrong with try/except here other than it's not to your personal taste?
Brett Cannon, one of the Python core devs, wrote a blog post using exactly this dict KeyError example in 2016 [1]. It concludes:
"The key takeaway you should have for the post is that EAFP is a legitimate coding style in Python and you should feel free to use it when it makes sense."
I would say that it's almost good if it wasn't for the Error word in KeyError.
If it was something like except KeyDoesNotExist: or KeyNotFound, it would make more sense for me, because it seems hacky to consider it an error where it's a normal default to some value behaviour.
Yes there's been various recommendations of this over the years and I think it's really bad.
Using try/except for conditional logic gives the developer a spurious choice between two different syntaxes to express the same thing. The reader is then asked to read try/except blocks as meaning two different things: either ordinary expected branching in the function, or handling exceptions.
I think it's definitely better if we just use conditionals to express conditionals, and try/except for errors, like every other language does. Here's some examples of where this duplication of syntax causes problems.
* Exceptions are often not designed to match the interface well enough to make this convenient. For instance, 'x in y' works for both mapping types and lists, but only mapping types will raise a `KeyError`. If your function is expected to take any iterable, the correct catching code will be `except (KeyError, IndexError)`. There's all sorts of these opportunities to be wrong. When people write exceptions, they want to make them specific, and they're not necessarily thinking about them as an interface to conveniently check preconditions.
* Exceptions are not a type-checked part of the interface. If you catch `(KeyError, IndexError)` for a variable that's just a dictionary, no type checker (or even linter?) is going to tell you that the `IndexError` is impossible, and you only need to catch `KeyError`. Similarly, if you catch the wrong error, or your class raises an error that doesn't inherit from the class that your calling code expects it to, you won't get any type errors or other linting. It's totally on you to maintain this.
* Exceptions are often poorly documented, and change more frequently than other parts of the interface. A third-party library won't necessarily consider it a breaking change to raise an error on a new condition with an existing error type, but if you're conditioning on that error in a try/except, this could be a breaking change for you.
* The base exceptions are very general, and catching them in code that should be a conditional runs a significant risk of catching an error by mistake. Consider code like this:
try:
value = my_dict[some_function()]
except KeyError:
...
This code is very seriously incorrect: you have no way of knowing whether 'some_function()' contains a bug that raises a KeyError. It's often very annoying to debug this sort of thing.
Because you must never ever ever call a function inside your conditional try block, you're using a syntax that doesn't compose properly with the rest of the language. So you can either rewrite it to something like this:
value = some_function()
try:
return my_dict[value]
except KeyError:
...
Or you can use the conditional version (`if my_dict[some_function()]`) just for these sorts of use-cases. But now you have both versions in your codebase, and you have to think about why this one is correct here and not the other.
The fundamental thing here is that 'try/except' is a "come from": whether you enter the 'except' block depends on which situations the function (or, gulp, functions) you're calling raise that error. The decision isn't local to the code you're looking at. In contrast, if you write a conditional, you have some local value and you're going to branch based on its truthiness or some property of it. We should only be using the 'try/except' mechanism when we _need_ its vagueness --- when we need to say "I don't know or can't check exactly what could lead to this". If we have a choice to tighten the control flow of the program we should.
And what do you buy for all this spurious decision making and the very high risk of very serious bugs anyway? Why should Python do this differently from every other language? I don't see any benefits in that article linked, and I've never seen any in other discussions of this topic.
You could do something like `value = table.get(some_function(), MISSING_VALUE)` and then have the conditional. But let's say for the sake of argument, yeah you need to assign the value up-front.
Let's say you're looking at some code like this:
if value in table:
...
If you need to change this so that it's `some_function(value)`, you're not going to miss the fact that you have a decision to make here: you can either assign a new variable, or you can call the function twice, or you can use the '.get()' approach.
If you instead have:
try:
return table[value]
except KeyError:
...
You now have to consciously avoid writing the incorrect code `try: return table[some_function(value)]`. It's very easy to change the code in the 'try' block so that you introduce an unintended way to end up in the 'except' block.
One thing that bothers me in my day-to-day Python work is that often libraries don’t document the errors that can be thrown by their functions. As the language lacks a “throws” statement, I found myself digging through library code on multiple occasions.
How do people approach this?
As much as I understand that checked exceptions (the general term for the 'throws' feature) can lead to a bit of a maintenance nightmare and doesn't necessarily scale well with deeper call stacks, I have found python documentation extremely lacking in this area too.
I think what I've come to understand is that you can treat errors you don't know about as non-recoverable, because that's most likely what they are anyway if they aren't readily documented. Let them bubble up basically, and handle them at the top level if necessary to prevent crashes in prod/make sure they are logged correctly and so on.
What they mean is that in Java, for example, a method has to explicitly state which exceptions it might throw as part of its signature. Note that they said "throws", not "throw".
Ah, I understand now. Well, by default Python doesn't declare a return type either yet the tools are able to infer it in many cases; I see no reason why tools like mypy couldn't similarly infer the raised exception types as well.
Plus, the typing annotations could presumably be expanded to include some notification to declare raised exception types explicitly.
Yes sure, but the raise statement is part of the implementation (like throw in Java), and not part of the signature (throws), which would presumably make it into a generated API documentation.
You can do nearly everything using Python errors. One thing that bothers me a bit, is the amount of text needed to catch them and bubble them up. Depending on the programming style traditional exceptions may also not be adequately re
Would you mind explaining what the benefits of either of those libraries is over normal exception handling? The examples at the top of the Result readme seem almost exactly as verbose as each other. I kinda like the idea of a Result object, but it seems like a lot of library imports for no gain so I'm sure I'm missing something.
Not OP, but as someone who likes this style of exception handling, I really appreciate that it offloads everything to the type system.
With normal exceptions, I have to use my meat brain to figure out if I've handled all the exceptions that might come up, whereas mypy can just tell me if I've handled the result object correctly
That said, I don't use either library since, in my experience, you need them to be used all the way down the stack to actually get the benefit.
> I have to use my meat brain to figure out if I've handled all the exceptions that might come up
The point of exceptions is exactly that you don't want to handle all that might come up -- you need to handle only those that you actually expect and know how to handle. Everything else is left to bubble up and either be handled by a custom, generic top-level handler, or by the language's default mechanism (in Python's case, dumping a stack trace).
It gives you control over the output of your program (and as you mention, the exact error code - which probably should be 1 in this instance).
It's often more user friendly to print a short human-readable message (or nothing) than to dump a huge stack trace. YMMV, if this is an internal dev tool then the opposite might be true!
I feel like both of them should crash the application. The only benefit is that you don't see the full stack trace in production, which depending on circumstances you don't want to be shown.
I tried it once in an sqlite DB connector with some business logic and simply checking stuff like
res: DBException | Result = db_handler.some_business_logic()
if isinstance(res, DBException):
return res # you can also log or even raise if this function isn't returning exceptions as values
# guaranteed to be Result type here
I think monad patterns can work pretty well in python, but my experience is nobody uses them.
I wrote a small utility library for exactly that kind of "railway oriented programming" if you're curious about what it'd look like in python: https://github.com/benrutter/ufo-tools
The article says basically "LBYL is bad", but this isn't a good description of what the author does in the later examples. Following "EAFP" without exception is also really bad.
The simple policy is that you should use "LBYL" when you're dealing with local state that can't change out from under you, especially when it's just properties of your local variables, but you need to use "EAFP" for anything that deals with remote state. Function calls can go either way, depending.
Blanket advice against LBYL leads people to use try/except blocks to express basic conditionals. If you want to ask something about your local variables, just ask it in a conditional --- don't make the reader infer "ah, these are the situations which will raise the error, and therefore we'll go into this block if the values are this way". If you want to do something when a key is missing from a dictionary, don't write:
Just write: (If you really need to avoid two lookups for efficiency, you would do something like value = table.get(key, MISSING_VALUE) and then check 'if value is missing_value'. But this optimisation will seldom be necessary.)The example the author gives about interacting with the file system is a good example of where indeed you really should use EAFP. The file system is remote state that your function does not own. Similarly if you're doing database operations, calling a remote API...lots of things.
There's various middle ground when you're calling functions. Often you don't want to worry about whether that function is going to be interacting with remote state, and you want to treat it totally as a black box, so you just use EAFP. But if the function documents really clear pre-conditions of your variables you can just go ahead and check, it's better to do that.