4 ms·
Error 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 t
by bb88 4y ago
Error 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.
- derefr 4y ago> A lot of production code uses urlparse() and it raises ValueError. https://docs.python.org/3/library/urllib.parse.html#urllib.p https://docs.python.org/3/library/urllib.parse.html#urllib.p... Yes, that was precisely my point: I don't understand why people are okay with this. URLs being invalid isn't an error; it's expected. If you're constructing a URL from a string, you haven't yet validated the URL. So why would it be exceptional for the validation to fail? I would expect explicitly calling a parser function, to give me the sum-typed result that the parser produces: either the successfully-parsed data, or the relevant shift/reduce failure. Consider: when I call urlparse(), I'm not demanding that the runtime construct a URL from the string. The function is not called `URL()`, like it is in Java (where IMHO it becomes perfectly justified to throw an exception — you expect `URL()` to give you a URL.) Calling a parse function like `urlparse()` does not imply that the thing I get back is going to be a URL. It implies that it's going to be the result of parsing, whatever that is. Just like, when I call `requests.get()`, I expect that the thing I get back is going to be the result of the request, whatever that is. > I ask because 1/0 is perfectly recoverable in most cases. It is recoverable, but only locally. No caller has any idea what to do with the fact that the callee divided by zero. It's a case where doing something and then not immediately handling it in the same local scope, is a programming error on the part of the library author. > What should the language do? raise an exception? return NaN? Panic? Core dump? Certainly, in dynamic languages like Python, programming errors in libraries have to be dealt with somehow. But it doesn't really matter what particular mechanism they use, because they shouldn't be recoverable by the caller. The callee failed the runtime's preconditions — like doing a use-after-free in C. The whole runtime is now poisoned, because the library, failing to handle this failure, very likely left its internal state in some indeterminate half-mutated form. This is just a too-late equivalent to a static language's "this fails to compile." So aborting the entire runtime — or doing anything else that means "this is unshippable as-is" — is probably the right choice. (Maybe delete any generated .pyc file for the module the code was defined in, while you're at it.) Maybe use a mechanism that can be caught by the runtime itself — if the language abstract-machine includes some sort of mobile-code-execution-sandbox abstraction like V8's isolates. Because programming errors in sandboxed user code shouldn't kill the host-runner. But that's the only case I can see where it'd be relevant. Ideally, in general, a dynamic language should do enough static analysis at load time to detect programming errors, such that it can have `import` itself raise, refusing to poison the environment with a module with axiom-invalidating semantics. (Though, in the case of integer division specifically, we've really just screwed ourselves over in most languages by making it a binary operator with a single integer result upon untrusted input. IMHO languages could do a lot better, by 1. having an explicit "integer divisor" linear-type that cannot be zero, and only allowing integer division by that type, such that you have to construct that type first; and/or 2. making integer division by regular can-be-zero integers a branching operation, where you must always define the code path for when the divisor is zero.) > Golang has two error channels, panics and return values, and I'd argue in this respect it's worse than Java. I disagree. Golang has one "error channel" — return values. You should never see/catch a panic from a callee. You should be able to just forget panics exist — similar to Errors in Java, or `error`s in Haskell. Golang panics have two uses: 1. the same thing I described above — unrecoverable operational states and runtime-detected programming errors in libraries. Abstract-machine axiom violations. Callers aren't supposed to handle/`recover` these. Nobody's supposed to touch these; they're supposed to just let them bubble, until the program halts. It doesn't matter that these are implemented via `panic` — they could just as well not bubble at all, and terminate the process on the very instruction they're raised on. They just-so-happen to use `panic` so that deferred functions get called for cleanup, so that sockets aren't left hanging open, files get flushed to disk, etc. 2. structured goto — like Ruby's `throw`+`catch`, (which have nothing to do with its exception system, which uses `raise`+`rescue`), or Common Lisp's `call/cc`. Golang JSON parsing, for example, uses `panic`+`recover` as a way to break out of parser stack frames until it hits the toplevel frame of the parse routing; so it can then immediately return a parse error to the caller. These are never supposed to leak outside the design of a library; so you're not supposed to be handling these, either. Also, this isn't error handling, per se; this is an optimization over returning a condition, then checking for that condition in the caller and breaking out of a loop and returning the same condition further, etc. It could just as well be used to signal a condition like "I'm done, I found what I'm looking for" as "I failed to parse." It's infrastructure, mechanism, that says nothing about the semantics of what it's trying to do or what type of information it's carrying. (Personally, I would argue that this should have been given a separate name from `panic`; and that `recover` should have only been able to catch these, not the axiom-violation panics of #1. But I understand that Golang's designers are big fans of avoiding special-purpose semantic primitives in favor of general-purpose "mechanism" primitives — as can be seen in their previous design of e.g. Plan9's everything-is-a-filesystem "mechanism" primitive.)