> 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.