4 ms·
The problem is the overloaded "some_data" / "some_error" return value. It's arguably worse semantics than just using exceptions. try: data = foo()
by bb88 4y ago
The 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.
- derefr 4y ago> The problem is the overloaded "some_data" / "some_error" return value. My point was that it's not "overloaded", because tuples returned for the purposes of matching aren't product-types (i.e. essentially objects but with unnamed positional fields, where the same position has the same meaning regardless of the other positions' values) but rather "mutually-exclusive-pattern-clause-matching conventional sum types" — the pure-data equivalent of the Result monad's `Left(Data) | Right(Error)`. Think Erlang's `{ok, Data} | {error, Error}`. With tagged tuples, you're not supposed to attempt to work with the tuple's fields independent of the whole — i.e. doing tuple[1] is a design smell, because it works regardless of whether you've got the tuple you expected or not. Rather, you're supposed to recognize the pattern that the tuple represents, including the tuple's "shape", and any specific literal tags; and then destructure the result into variables only if the pattern fully matches. Tuples and pattern-matching go together. (And it's kind of weird that Python got one long before the other.) Note that there's no real difference between using tuples here, and using plain data-carrier classes. A type-tag as a positional tuple parameter is not trying to do anything different than an object with an explicit type is; tagged tuples just avoid needing to create little bespoke data-carrier classes for every possible little returning-an-enum-of-patterns situation that crops up a thousand different places in a codebase. > Would be arguably better to write: [raise the error] Why? Just because a piece of data "represents an error", doesn't mean that it's a programming/logic/operational/runtime error in the semantics of your system. It's just data you're carrying around, that represents a certain condition. Maybe a condition you're modelling in some other system. Maybe a condition that's equally common to the "data" path. Picture, for example, implementing an HTTP caching gateway, like Varnish. Receiving a 4xx/5xx response from the origin isn't an error for Varnish — rather, it's an entirely expected condition. An "HTTPErrorResponse" is just a type of data that Varnish works with — caching it, serving it, etc. I recently wrote a Python function called is_domain_trustworthy. It returns True | (False, reason_class, reason_detail). In theory, this function could raise these as errors... but it's not a validation function; it's a predicate. It's answering a question. It wants to answer that question with a yes or no; it's just that in the False case, it also has some extra detail it can pass along about why it decided the domain wasn't trustworthy, which can be used for debugging / audit logging. This "yes or no-and-why" answer to the question is just data. That computed data gets put into a database, even. You can certainly build a validation function on top of this predicate. You'd do so by writing a pattern-match, where one or more arms of the pattern-match raises an exception. But personally, I find it extremely useful to have the "plain" version of the function not raise, but just give me data. More flexible in code; much simpler to work with in the Python REPL; etc. > resp.raise_for_status(); resp.ok A difference with these, is that these are fundamentally examples of a library author trying to predict what the library's users might want to treat as semantic error conditions. Mutually-exclusive pattern matching, on the other hand, doesn't require that the data is modelled to "bake in" any knowledge of conditions users might care about. You can arbitrarily group patterns together. For an example: match resp: case Response(status_code=code) as long_resp if code in [200, 500, 502, 503, 504]: # these requests take a nontrivial amount of time, so capture the request in a histogram case _: pass Note how there's nothing on either arm here that's anything like an error condition. It would never make sense to have a library know about this condition, let alone to treat it as an exception. It's just a pattern, where matching that pattern means your data now takes on some additional meaning.
- bb88 4y agoError classes will always be second class citizens in python and you'll lose them if a first class exception is thrown. Worse people using your code now have to have two kinds of patterns dealing with errors: exceptions and tuple unwrapping, lest you can guarantee your code will never raise exceptions.
- derefr 4y ago> lest you can guarantee your code will never raise exceptions I mean, if you're writing a predicate function — or any other function that "just returns data" — then yes, you certainly should guarantee that it never raises an exception†. It should be a pure function. Anything else would be extremely annoying. How Python people deal with the fact that simple things like dictionary key lookup or URL parsing raise exceptions without thinking of that as a language-design error, I'll never understand. Give me a language where functions return types like Option that have to be explicitly acknowledged and handled exhaustively, any day, over Python's "code written only for the happy path that nevertheless works without warnings, right up until it ever gets fed an invalid input, upon which point it crashes, because nothing ever forced you to handle that error, or even indicated that you would want to." † Or, at least, it shouldn't be throwing any exception your caller would ever expect to need to catch as part of the semantics of using your library. What Python calls "errors" and Java calls "unchecked exceptions" — e.g. MemoryError and the like — are fine to let "leak out" of pure functions, because those exceptions aren't really intended for the direct caller to handle; they're intended to skip right past the caller, to whatever top-level framework code created the thread that the caller is running in. (See also: Haskell's `error` function, which, despite ML-alikes heavily favoring sum-typed return types, nevertheless exists, for just these same cases.)
- bb88 4y agoI'm not arguing against the power of the error classes. I argue that you're turning python into golang, whose error handling is arguably worse than python or Java. > How Python people deal with the fact that simple things like dictionary key lookup or URL parsing raise exceptions without thinking of that as a language-design error, I'll never understand. "if "x" in dict", or "dict.get("x")" works really well. But again, if you want the exception, it's there. "dict["x"]" will raise. You also have sets in python so you can do stuff like this: potential_keys_in_d = set(d.keys()).intersection(potential_keys) potential_keys_not_in_d = set(potential_keys) - set(keys.d()) A lot of production code uses urlparse() and it raises ValueError. https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlparse https://docs.python.org/3/library/urllib.parse.html#urllib.p... > Or, at least, it shouldn't be throwing any exception your caller would ever expect to need to catch as part of the semantics of using your library. So your code does 1/0. What should the language do? raise an exception? return NaN? Panic? Core dump? I ask because 1/0 is perfectly recoverable in most cases. In C it's undefined (core dump anyone?). Golang panics. Python and java raise an exception. And javascript returns ... well ... something worse. > What Python calls "errors" and Java calls "unchecked exceptions" — e.g. MemoryError and the like — are fine to let "leak out" of pure functions, because those exceptions aren't really intended for the direct caller to handle. From the standpoint of a being a pragmatic programmer, the language just needs to choose one error channel and let that be the primary form of error passing. Golang has two error channels, panics and return values, and I'd argue in this respect it's worse than Java.