Hacker News new | ask | show | jobs
by mindslight 2686 days ago
It amazes me how enduring formulaic it is to single out some particular design tradeoff of a language, draw up some examples of expressing something where that tradeoff creates worse code, and then act like it's some mortal flaw in the language.

Python chose untyped exceptions, period. How is this surprising, given that its basis is untyped parameters?

If you don't like that, use Java with its checked exceptions. Or remove exceptions from the implicit monad altogether and use C or Rust. Just don't then go on to write some lengthy post about how any one of those is too explicit.

5 comments

Do you mean unchecked exceptions? Python exceptions are strictly typed, that's core to how they work. An except clause will catch subclasses of the target, so you need to have a custom type for each exception you want to raise in your code.

The mistake I see people make is to use built-in exceptions without subclassing, making it impossible to explicitly catch specific errors. Or the opposite, catching Exception, which will catch everything indiscriminately.

No. I mean checked exceptions. At the language level, Java checked exceptions are typed, and unchecked exceptions are untyped.

The Python runtime is dynamically typed. The Python language is untyped.

Both checked and unchecked exceptions are typed in Java. Unchecked exceptions are not included in the method signature, but that doesn't mean that the exceptions are untyped.
The exceptions, meaning the runtime objects themselves, are indeed typed.

But at the language level, unchecked exceptions are not part of method signatures (as you said), therefore not type- checked/declared/inferred. Therefore appropriately described as untyped -

When calling a method, you have no (formal) list of unchecked exceptions that might be raised.

If you think of exceptions as being an implicit union type around every function return (ie monad), analogous to how you have to explicitly check for errors in C/Rust, you'll see what I mean. Java's unchecked exceptions are akin to calling a function in Python that you expect to return objects of only one type, but not being "sure" that it can't return something else.

The author puts an exception in a didactic example of a "division" function. Then upon finding that the exception cannot easily generalize to all possible use-cases of the didactic example the author concludes:

> So, the sad conclusion is: all problems must be resolved individually depending on a specific usage context.

Why is that sad/revelatory? If the wrapper simply reproduces the behavior of a single infix operator for floating point numbers, then almost by definition one would need to reimplement NaN in order to get a general purpose exception handler.

Exactly. Besides, I fail to see how exceptions are the problem in there.

> Exceptions are hard to notice

No. In Java they are easy to notice. In Python we have a dynamic language, so there is no safety net for that. It's a limit of the language that we chose, by design, with pros and cons. It's not exception related.

> So, the sad conclusion is: all problems must be resolved individually depending on a specific usage context. There’s no silver bullet to resolve all ZeroDivisionErrors once and for all

That's a good thing. There are many reasons to have a ZeroDivisionErrors, and they beg all for a different solution. Sometime it's you, sometimes it's the user, sometime it's just how things are. Again, it's not related to exceptions in any way.

> For example, the system might notify the user to change the input, because we can not divide by 0. Which is clearly not a responsibility of the divide function

Indeed, you are supposed to sanitize your inputs at the intersection between your program and the rest of the world. No surprise, this has nothing to do with exceptions.

> Now we just need to check where this exception is actually caught. By the way, how can we tell where exactly it will be handled?

Because you write the handling code. If you don't write error code handling, how is that the exception system fault ?

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

Yes, there is. Either you handle it, and the next step is in the try/except, or you don't, in which case your program crashed. Same with any way of dealing with errors. It's not about exceptions.

> We have two independent flows in our app: regular flow that goes from top to bottom and exceptional one that goes however it wants.

Nope, the exception flow goes from the bottom of the stack to the top. It's well defined, we even have a beautiful stack trace for that. Don't blame exceptions if you don't even know the basics.

> Exceptions are not exceptional

Compared to the rest of the lines of your program, they are. Exceptions are called that way because they represent a special case. The fact computing is full of them is, you know it now, not related to exceptions.

> How to be safe?

Handle the error. Like in any language, with any error handling tool. There is a difficulty in Python: the exceptions are not listed in the function signature. That's hardly the fault of the concept of exception, and is just a design compromise.

> Now you can easily spot them! The rule is: if you see a Result it means that this function can throw an exception. And you even know its type in advance.

So basically you rewrite Java in Python. Again, not a problem with the exceptions. And oh, no, don't do that. If you want that, use Java, not Python. It's perfectly reasonable, but don't turn Python into Java. The fact exceptions are not written in the function signature is, I repeat, a design decision. It has pros. It has cons. But don't use a screw driver like a hammer, that's bad.

> ow to work with wrapped values?

Ok, now you are trying to implement Haskell in Python.

Use Haskell then.

Python is not made for this. It's an easy to write and read language. You are supposed to be able to edit Python code with notepad if you have to. Every line should be short and to the point, with a well defined role.

And we expect exceptions to bubble, that's how we like it.

> But how to unwrap values from containers?

See what I mean ? The author just opened the pandora box. Now he or she has to write long chunks of text just to explain the basic of error handling.

Without this system, I can just pdb.set_trace() in there, now it's full of inlines, chaining, anonymous callbacks and wrappers.

That's just... no.

> Python is not made for this.

Maybe, but there are ungodly big and crufty codebases in Python all around the world, and a lot of stuff depends on them. And it turns out that Python was never meant to be the thing that is being depended on. That's unfortunate. Hence the whole gradual typing (mypy) and other kinds of safety efforts.

You're not forced to use this. But it seems pretty useful, even for scripts.

I see a lot of commenters here are as perplexed as I was when I first read this post.

The key thing to understand here is that post is really about an attempt to implement the "Railway Oriented Programming" paradigm [1] by Scott Wlaschin, but in Python. I would suggest reading the Scott's post on ROP and at least skimming the video before going on.

So ROP is, as Scott himself states, a way to take all those nice Haskell concepts and techniques and apply them in F# in a way that won't overwhelm those who are new to them.

The problem with the original post is that it presents two problems with exception in Python and then offers the "returns" library as a solution which, ultimately, doesn't end up solving either of those problems.

The first problem the post describes is that the exceptions are not part of the function signature. The second problem is that exceptions are essentially gotos and that this makes reasoning about the execution flow very difficulty.

To tackle both problems, the returns library offers its own implementation of the Either monad in form of the Result container. Having presented that solution, the post promptly decides that monadic code in Python is unreadable and offers the @pipeline decorator which allows you to write code that looks imperative, which partially defeats its purpose as a solution to the problem about the flow reasoning. I say partially, because it replaces implicit gotos with implicit returns, which is a marginal improvement over exceptions.

The post then decides that using the Result class in the signature is also ugly and offers the @safe decorator which allows you to write the same code you would write without ROP, but now it wraps everything in Result behind the scenes. Even worse than that, this produces the return type of Result[whatever_your_success_type_is, Exception]. For those familiar with Java, this is very similar to putting "throws Exception" in your method signature, except that it's hidden and implicit.

I'll end this with a bit of advice for the author, preceded by an apology, because I'm not sure if I have managed to find a way of phrasing it that doesn't sound harsh. You might want to do some serious reading about functional programming and monads and how these concepts have been carried over into and grafted onto mixed-paradigm languages like Java. I mention Java specifically, because one of the complaints in your post is that Python won't be supporting checked exceptions in the nearest future. A lot has been written about checked exceptions in Java -- on both sides of the discussion.

[1]: https://fsharpforfunandprofit.com/rop/ [2]: https://en.wikipedia.org/wiki/Tagged_union

Well or solve the perceived problem and use the solution (their "returns" library)...