10 ms·
> If you instead raise exceptions, you are immediately forcing the calling code into a special control flow structure, namely the try/except dance, which can be
by dahfizz 4y ago
> If you instead raise exceptions, you are immediately forcing the calling code into a special control flow structure, namely the try/except dance, which can be inconvenient.
I fail to see how a try/catch is more inconvenient than a handful of isinstance calls.
What happens when you want to add a new error case? With exceptions, the exception will always be thrown. With error objects, you're going to fall through a bunch of isinstance checks and continue on with a result object which isn't the type you think it is. Then, some time later, youre going to try and reference a field that doesn't exist. This is the kind of thing which is a nightmare to debug.
I do appreciate the author's point that error objects are nice in functional programming languages. But using them in Python because you like using them in fp is not a good justification.
- dragonwriter 4y ago> With error objects, you're going to fall through a bunch of isinstance checks and continue on with a result object which isn't the type you think it is. If you are doing isinstance checks (or pattern matching or something else equivalent) for separating error/success returns where, as in Python, you don't have exhaustiveness checks, your default case should be treating the result as an unknown error, not a success. Also, this error can be avoided on the called-function side by using a common type (concrete or root) for errors. Using a wrapper for errors, as with a Result type (whether or not successes are also wrapped as they would be with a result) achieves this, for instance.
- dahfizz 4y agoYes, having a default case that throws an Unknown Error is the correct thing to do here. But that is still less ergonomic. For an unhandled exception type, the person creating the exception can attach a helpful error message. For an unhandled exception object, you can't do any better than "An Unexpected Error Occurred". You can do a little better by having multiple error subclasses. But at that point you're literally reinventing exceptions, but with a greater onus of the caller to do the right thing. This seems to go against the "pit of success" idea from the article. There's no way for the developer to accidentally miss an exception, or misunderstand the subtleties of the error inheritance structure.
- angarg12 4y agoDoesn't Python 3.10 add pattern matching? [1]. Seems to make this much more palatable. Specifically look at [2]. [1] https://peps.python.org/pep-0636/ https://peps.python.org/pep-0636/ [2] https://peps.python.org/pep-0636/#adding-a-ui-matching-objects https://peps.python.org/pep-0636/#adding-a-ui-matching-objec...
- dahfizz 4y agoYes it does! It's such a nice addition to the language. It does make error objects more palatable, but I still don't see the benefit of error objects over exceptions. You still lose stack traces, and a universal way to get error messages. You're still taking a risk that the caller will miss a case and not handle the error at all, thereby shrinking the "pit of success". Also, FWIW, the code in question is 5 years old. It seems to be more of an idealogical question than a pragmatic one.
- tialaramex 4y agoExceptions come with program flow. Didn't need/ want that flow? Too bad, you get it anyway and must re-structure your program accordingly. Suppose there are two consumers of my function F, which usually returns a Digit but sometimes instead has a problem. Should F throw ProblemException or should its return type be a Sum Type with both Digit and Problem ? Alice uses F in some hot code that doesn't immediately care about the problem or the digits, while Bob is only using F in an edge case and needs the result immediately. For Alice, needing to try F and handle the exceptions (which Alice doesn't ever care about immediately) is annoying. For Bob it works fine. But, Bob can also write code to handle the Problem next to his code for handling the Digit, we were forcing Alice to handle ProblemException, but Bob can choose where to do something with Problem as it suits him.
- mafro 4y agoAlice isn't forced into using anything. You appear to be forgetting that raising exceptions as part of program flow is standard and pythonic. It's OK to disagree with that, but as it's part of the language and part of the python modus operandi let's not go out of our way to illustrate our dislike.. Just use another language
- NovemberWhiskey 4y agoYeah; this proposal is pretty painful-looking to me, as a Python programmer. How do you do finally logic with this pattern?
- CaptainNegative 4y agoThere are legitimate cases in Python where you want the equivalent of "On Error GoTo Next", which currently requires a blowup of one statement into four (try, f(), except, pass). Returning an error can simplify such cases. Sure, ignoring exceptions can be (horrid) code smell, but in low-stakes contexts it's not necessarily bad code. You see this especially often in the context of one-off Jupyter notebooks that data scientists like writing, where quick iteration, flexibility, and ease of reading is more important than making robust, production-ready code. Did my text scraper fail because of a network error or was it just some weird unicode character that broke it? Don't care. I just want some collection of simple english text to train my language model, and there's more than enough else on the internet for me to fuss about corner cases.
- NovemberWhiskey 4y ago>which currently requires a blowup of one statement into four (try, f(), except, pass). Or ... with suppress(Exception): flaky_thing() ... assuming modern Python.
- przemub 4y agoThat's nice, thanks for sharing that.
- bb88 4y agoMore importantly is the ability to subclass exceptions, so it doesn't break client code. class MyBaseException(Exception): ... class MyExistingException(MyBaseException): ... class MyBrandSpankingNewException(MyBaseException): ... The client can catch the base exception no matter what new exceptions you create in the future.
- tialaramex 4y agoThis design claims MyBrandSpankingNewException was: * So different we just couldn't represent it as MyExistingException and yet * So similar that it's fine to just treat it as MyBaseException everywhere In my experience everywhere I see this pattern all we're doing with MyBaseException is giving it to a human, via logging, stderr, or whatever, which means this is a lot of complexity to just move some text. Although this strategy didn't formally "break client code" in practice if it matters the code will need to be rewritten to actually handle MyBrandSpankingNewException.
- d0mine 4y agoUsually a derived exception is a special case of the base exception e.g., OSError -> FileNotFoundError. Sometimes you want to handle specific error differently and sometimes you don't care. btw, it may be the reason to create a new subclass, to distinguish between cases when you do care.
- derefr 4y agoI don't think typed errors + isinstance checks are really the alternative. As of Python 3.10, I can just write the same Result-monad-ish destructuring-pattern-matching code in Python that I do in e.g. Elixir: def foo(): if whatever: return (True, some_data) else: return (False, some_error) def use_foo(): match foo(): case (True, good_data): ... case (False, ExpectedErr() as err): ... case (False, any_unexpected_err): ... Why bother? Because often, you'll write functions where both "good data" and "bad data" are just data — for example, a classify_spam() function returning (category, confidence_score). Neither case is exceptional; both cases are expected. But you have to handle them separately. A great example would be an HTTP request-response: match requests.get(...): case Response(status_code=code) if code >= 300 and code < 400: # do a redirect It would make absolutely no sense for 3xx responses to raise an exception, no? But you do want to handle them separately from the regular flow of code. You want to be dealing with a categorized return object, where you're forced to recognize the category of it — to match on it — in order to do anything further with it. (I do wish that the default for `match` was to blow up with a ValueError if you run off the end, rather than being a no-op, though. As it stands, you have to explicitly add `case _: raise ValueError(...)` clauses if you want to be strict.)
- bb88 4y agoThe problem is the overloaded "some_data" / "some_error" return value. It's arguably worse semantics than just using exceptions. try: data = foo() except SpecificException as err: ... except BaseException as err: ... except Exception as unhandled_exception: ... It doesn't really look that different than your code above. Further: return (False, some_error) Would be arguably better to write: raise NoBarError(f"Bar wasn't found in file {f}") > match requests.get(...): Speaking of requests I actually like the requests API here. It always gives you a response object, but allows you to do this: resp = requests.get(...) resp.raise_for_status() # raises an exception on 400 and above. It doesn't force you into exceptions, but allows you to choose how you want your errors. The reason is because resp.content may still be meaningful even if resp.status_code is 500. Further it gives you: resp = requests.get(...) if resp.ok: Error and data are not overloaded on the return value. They are a part of the same class.