DEV Community

Discussion on: Python exceptions considered an anti-pattern

Collapse
 
rhymes profile image
rhymes • Edited

I think the context in this discussion is everything. I'll explain:

And checked exceptions won't be supported in the nearest future.

I feel like this is a pro in the context of Python though, not a con. Guido Van Rossum talks about it in the thread you linked. Java's checked exception aren't great for usability as he hints at. In theory they are a safe idea, in practice they encouraged suppression, rethrowing, API fatigue over the years. A "simple feature" that really polarized Java programmers :D

On the other side, Go designers have basically decided to force programmers to handle errors all the time (though you can still supress them) but the ergonomics is still not perfect (and they are thinking of making adjustments for Go 2.0 using a scope-wide central error handler)

There's no way to tell which line of code will be executed after the exception is thrown.

I'm not sure I follow. The stack for unhandled exceptions tells you where you were, the rules for handled exceptions are quite clear:

try:
  1 / 0
  print("Hello") # this is never going to be executed
except ZeroDivisionError as e:
  # do something with the error
  pass

print("World")
Enter fullscreen mode Exit fullscreen mode

This is going to print only World. Where is the unclear part?

How can we consciously read code like this?

This sentence is a little odd, again in context, because Python programmers have been doing it since 1991. It's not yesterday :D I'm not saying the status quo is perfect, but sentences like "how can we deal with it" hide the fact that a lot of people have been doing it fine for decades.

Exceptions are just like notorious goto statements that torn the fabric of our programs.

Maybe, but not exactly. Goto was a bad idea because it let you jump to any place in the code at any time. Exception don't do that. Also, the "goto(ish)" (context again matters here) aspect of exceptions really depend on you as the developer. If you don't actively used them as a control flow mechanism but only to handle errors, then they are error handlers.

Now we got that exceptions are harmful to your code.

It's still your opinion though, I'm reading your post and I disagree :-D.

it is not possible to wrap all your possible errors in special-case classes. It will require too much work from a developer. And over-complicate your domain model.

Mmm again, people have been wrapping their errors in custom exceptions for decades, why are you suddenly saying it can't be done? I'm not saying it's the best strategy, I dispute your "cants" and "harmfuls" and "wrongs". By the way creating custom exceptions is so standard practice that it's even suggested in the tutorial.

In general: I'm not saying exceptions are the best ever method of handling error situations and I understand my bias as an enthusiastic Python developer (though my list of stuff that I don't like about Python has some items, my pet peeve with exceptions was recently fixed) but the way you wrote it sounds like this: "this pretty fundamental and central feature of Python is trash, I wish it were different. Here how you should do it, my way is better".

Going back to the idea co context: if I was going around saying "goroutines in Go are bad" I might be right in a context (it takes a few lines of Go to create a leak in the abstraction :D) but I would also ask myself if Go is the right choice for me. There's nothing wrong with a good rant (which this sounds like) or not liking features but at the end of the day we have only one life. Instead of trying make a language conspicuously deviate from its definining principles (aka the reason we're not all using the same language), why not just use another? We don't have to be "mono language".

The idea of wrapping everything in what basically are option types (if I'm not mistaken, I'm not strong on type theory and polymorphic typing) seems to veer quite a bit away from "Python's zen" ethos which are quite explicit on the idea that errors shouldn't be silent (which you're basically doing by discarding raise in favour of an option type) and to favor simplicity above all (which is probably less evident by looking at the seemingly innocous decorators in the last example)

I don't see the idea of having to unwrap values and exception constantly as an improvement to be honest :D The only advantage I see here is the compile time check, but it worsen the readability of the code and the mental gymnastic any Python developer has to do (considering that your proposal has not been built in in Python since day one), even with the decorator based shortcuts.

That's what I meant by citing context and language design in the beginning. Your idea is not bad per se, it's just probably not the right one in the context of Python's design. It reminds me a little of the argument in favor of removing the GIL :D

"Exceptions are hard to notice". Now, they are wrapped with a typed Result container, which makes them crystal clear.

But it's not actually true, is it? Let's look at your example:

def __call__(self, user_id: int) -> Result['UserProfile', Exception]
Enter fullscreen mode Exit fullscreen mode

Here you're telling the reader that the method can raise an exception. What new information I have than I didn't know before? Knowing that any method can return exceptions in Python?

Also, this method is telling me that I basically have to prepare for any type of exception, which makes the advantage of the declaration moot if we put aside mypy and the tools for a second: what's the gained advantage of this instead of using a comment/documentation that says: this method can raise a HTTP error or a parsing error?

Tools are important but my brain the first time it read this code said: "well, thanks for nothing, Exception is the root class, anything can throw an exception".

If Python was a statically typed language I would probably have a different opinion about all this but it isn't, and the types can be ignored (and are ignored by default) which by being optional only leave the constant of the programmer reading this code in 5 years and being told something they already know. Again, context is everything here.

The TLDR; of all this long response is that the role of a language designer is to find tradeoffs and in the context of Python exceptions work. Stepping out of them would be changing the language, which could be perfectly fine in theory, but are we really sure?

I'm so glad I'm not a language designer, it's a tough job, and now I'm starting to understand why Guido semi retired ;-)

Collapse
 
sobolevn profile image
Nikita Sobolev • Edited

@rhymes wow! That's a very detailed and deep opinion. Thanks for bringing this up.

There's no way to tell which line of code will be executed after the exception is thrown.

Let me give you another example. Imagine that you have a big library / framework / project. And you are just debugging it. And then your execution flow jumps to some random place in the code with an exception thrown somewhere inside the method you were jumping over. "What the hell happened?" is what I usually have in my mind at this moment.

And that's what make me feel like we have two executional flows in the app. Regular and exception flows. The problem that I see in this separation that we have to wrap our minds against these two fundamentally different flows. First is controlled with regular execution stack and the second one is gotoish (as you called it). And it is controlled with except cases. The problem is that you can break this second flow by just placing some new awkward and accidental except case somewhere in between.

And, while I can perfectly live with the exceptions, I start to notice that my business logic suffer from it when it is growing in size. And having an extra way to work with different logical error in your program is a good thing.

it is not possible to wrap all your possible errors in special-case classes

I was talking about special classes like AnonymousUser is django. Not custom exception classes.

seems to veer quite a bit away from "Python's zen" which are quite explicit on the idea that errors shouldn't be silent

Unless explicitly silenced!

Jokes aside, we are not making them silent with wrapping into container values, just making them wrapped. Later you can use them anyway you want. You can reraise them if that's how it should be done.

Here you're telling the reader that the method can raise an exception

Yes, exactly! All functions may raise. But, some functions are most likely to do it: when dealing with IO, permissions, logic decisions, etc. That's what we indicate here. And, yes, my example with Result['UserProfile', Exception] should be rewritten as Result['UserProfile', ExactExceptionType].

When exceptions become the important part of your logic API - you need to use Result type. That's a rule that I have created for myself. Not talking about just python, but also elixir and typescript.

Thanks for taking a part in my rant, I have really enjoyed reading your response ;)

Collapse
 
rhymes profile image
rhymes

Let me give you another example. Imagine that you have a big library / framework / project. And you are just debugging it. And then your execution flow jumps to some random place in the code with an exception thrown somewhere inside the method you were jumping over. "What the hell happened?" is what I usually have in my mind at this moment.

I know what you're talking about, I've been there but to me is more an issue of abstraction than of exception handling. Some apps have too many deps :D BTW how is that super different from calling a function?

import template_library
def method_to_do_something():
  a = "Hello world"
  result = template_library.render(a=a)
  # do something else...
  return result
Enter fullscreen mode Exit fullscreen mode

in this example I jump from method_to_do_something to template_library.render and back everytime I debug. And then if render calls yet another library the thing goes on. Sometimes you spend minutes stepping in libraries because the dependency tree is long (Ruby with open classes, heavy use of metaprogramming and DSLs sometimes is a nightmare to debug)

If you think about it, in a way, functions are labeled gotos with an address to go back to ;-)

and, yes, my example with Result['UserProfile', Exception] should be rewritten as Result['UserProfile', ExactExceptionType].

ok, that's make a little more sense :D

That's a rule that I have created for myself. Not talking about just python, but also elixir and typescript

Got it. Programmers are strange animals, when they find a tool or an idea they like they want to apply to everything :D.

Thanks for taking a part in my rant, I have really enjoyed reading your response ;)

ahhaha :D