Sometimes, you have an API: @abc.abstractmethod def get_property_value(self, prop): """Returns the value associated with the given property. Args: prop (DataProperty): The property. Returns: The value associated with the given property. Raises: PropertyError: If the property is not supported by this config source. LookupError: If the property is supported, but isn't available. ValueError: If the property doesn't have exactly one value. """ raise PropertyError and you don't want your API to mask bugs. How would it mask bugs? For example, if API consumers do an: try: x = foo.get_property_value(prop) except ValueError: # handle it then the following implementation: def get_property_value(self, prop): iterator = self.get_property_values(prop) try: # note: unpacking ret, = iterator except LookupError as exc: raise RuntimeError from exc # don't accidentally swallow bugs in the iterator return ret def get_property_values(self, prop): try: factory = self.get_supported_properties()[prop] except KeyError as exc: raise PropertyError from exc iterator = factory(self._obj) try: first = next(iterator) except StopIteration: return (x for x in ()) except abdl.exceptions.ValidationError as exc: raise LookupError from exc except LookupError as exc: raise RuntimeError from exc # don't accidentally swallow bugs in the iterator return itertools.chain([first], iterator) could cause you to accidentally swallow unrelated ValueErrors. so instead this needs to be rewritten. you can't just unpack things from the iterator, you need to wrap the iterator into another iterator, that then converts ValueError into RuntimeError. but if that ValueError is part of *your* API requirements... it's impossible to use! so my proposal is that we get "exception spaces". they'd be used through the 'in' keyword, as in "except in" and "raise in". for example: X = object() Y = object() def foo(): raise LookupError in X def bar(): try: foo() except LookupError in Y: print("bar: caught LookupError in Y, ignoring") def baz(): try: bar() except LookupError in X: print("baz: caught LookupError in X, re-raising in Y") raise in Y def qux(): try: baz() except LookupError in Y: print("qux: caught LookupError in Y, ignoring") qux() # would print: # --- # baz: caught LookupError in X, re-raising in Y # qux: caught LookupError in Y, ignoring # --- # note the lack of "bar" (or perhaps "raise in X LookupError" and "except in Y LookupError" etc) and then you can adjust the above implementations accordingly: (btw, anyone knows how to tell apart a ValueError from a specific unpacking and a ValueError from an iterator being used in that unpacking?) def get_property_value(self, prop, espace=None): iterator = self.get_property_values(prop, espace=espace) try: # note: unpacking ret, = iterator except ValueError: raise in espace # except LookupError as exc: raise RuntimeError from exc # no longer needed return ret def get_property_values(self, prop, espace=None): try: factory = self.get_supported_properties()[prop] except KeyError as exc: raise PropertyError in espace from exc iterator = factory(self._obj) try: first = next(iterator) except StopIteration: return (x for x in ()) except abdl.exceptions.ValidationError as exc: raise LookupError in espace from exc # except LookupError as exc: raise RuntimeError from exc # no longer needed return itertools.chain([first], iterator) as well as the caller: espace = object() try: x = foo.get_property_value(prop, espace=espace) except ValueError in espace: # handle it I feel like this feature would significantly reduce bugs in python code, as well as significantly improve the error messages related to bugs. This would be even better than what we did with StopIteration! This would be comparable to Rust's Result type, where you can have Result<Result<YourValue, YourError>, TheirError> and the like (except slightly/a lot more powerful).
On Thu, Apr 9, 2020 at 5:31 PM Soni L. <fakedme+py@gmail.com> wrote:
Sometimes, you have an API:
SNIP
Raises: PropertyError: If the property is not supported by this config source. LookupError: If the property is supported, but isn't available. ValueError: If the property doesn't have exactly one value. """ raise PropertyError
and you don't want your API to mask bugs. How would it mask bugs? For example, if API consumers do an:
try: x = foo.get_property_value(prop) except ValueError: # handle it
SNIP
If you don't want standard Python exceptions, such as ValueError to be confused with exceptions from your own app, just create your own custom exceptions such as class MyAppValueError(Exception): pass and raise these custom exceptions when necessary. No need to change Python or use convoluted logic. André Roberge
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/J76S2N... Code of Conduct: http://python.org/psf/codeofconduct/
Hello, On Thu, 9 Apr 2020 17:40:26 -0300 André Roberge <andre.roberge@gmail.com> wrote:
On Thu, Apr 9, 2020 at 5:31 PM Soni L. <fakedme+py@gmail.com> wrote:
Sometimes, you have an API:
SNIP
Raises: PropertyError: If the property is not supported by this config source. LookupError: If the property is supported, but isn't available. ValueError: If the property doesn't have exactly one value. """ raise PropertyError
and you don't want your API to mask bugs. How would it mask bugs? For example, if API consumers do an:
try: x = foo.get_property_value(prop) except ValueError: # handle it
SNIP
If you don't want standard Python exceptions, such as ValueError to be confused with exceptions from your own app, just create your own custom exceptions such as
class MyAppValueError(Exception): pass
and raise these custom exceptions when necessary. No need to change Python or use convoluted logic.
... And if you have a problem with a 3rd-party lib, drop them a bug report. And if they don't listen (everybody is smart nowadays and knows better how it should be), just fork that lib, and show everyone how to do it right... [] -- Best regards, Paul mailto:pmiscml@gmail.com
On 10/04/20 8:30 am, Soni L. wrote:
Sometimes, you have an API:
@abc.abstractmethod def get_property_value(self, prop): """Returns the value associated with the given property.
Args: prop (DataProperty): The property.
Returns: The value associated with the given property.
Raises: PropertyError: If the property is not supported by this config source. LookupError: If the property is supported, but isn't available. ValueError: If the property doesn't have exactly one value. """ raise PropertyError
This doesn't really look like a Python API. It's fairly rare in Python for exceptions to be used to indicate anything other than "something unexpected went wrong". In your unpacking example, the ValueError isn't really intended as something to be caught under normal circumstances. The assumption is that you'll know how many items to expect when you unpack something, and if you don't get that many, then you have a bug. You could advocate for unpacking to use a more specific exception to facilitate catching it, but I think most people will consider your use case to be quite rare.
so my proposal is that we get "exception spaces". they'd be used through the 'in' keyword, as in "except in" and "raise in".
I don't think exceptions are used this way in Python commonly enough to justify this. -- Greg
On 2020-04-10 7:05 a.m., Greg Ewing wrote:
On 10/04/20 8:30 am, Soni L. wrote:
Sometimes, you have an API:
@abc.abstractmethod def get_property_value(self, prop): """Returns the value associated with the given property.
Args: prop (DataProperty): The property.
Returns: The value associated with the given property.
Raises: PropertyError: If the property is not supported by this config source. LookupError: If the property is supported, but isn't available. ValueError: If the property doesn't have exactly one value. """ raise PropertyError
This doesn't really look like a Python API. It's fairly rare in Python for exceptions to be used to indicate anything other than "something unexpected went wrong".
In your unpacking example, the ValueError isn't really intended as something to be caught under normal circumstances. The assumption is that you'll know how many items to expect when you unpack something, and if you don't get that many, then you have a bug.
You could advocate for unpacking to use a more specific exception to facilitate catching it, but I think most people will consider your use case to be quite rare.
so my proposal is that we get "exception spaces". they'd be used through the 'in' keyword, as in "except in" and "raise in".
I don't think exceptions are used this way in Python commonly enough to justify this.
it's actually fairly common to deal with KeyError instead of using dict.get or w/e. KeyError is also raised when your code has a bug and your assumptions got broken. it's fairly easy to get the two different KeyErrors mixed up, at least. idk if this is the case for other exception types, but I'd expect a few other exception types might also commonly be used in this way.
On 10/04/2020 12:43, Soni L. wrote:
it's actually fairly common to deal with KeyError instead of using dict.get or w/e.
KeyError is also raised when your code has a bug and your assumptions got broken.
it's fairly easy to get the two different KeyErrors mixed up, at least.
So don't do that, then. -- Rhodri James *-* Kynesim Ltd
On 2020-04-10 9:40 a.m., Rhodri James wrote:
On 10/04/2020 12:43, Soni L. wrote:
it's actually fairly common to deal with KeyError instead of using dict.get or w/e.
KeyError is also raised when your code has a bug and your assumptions got broken.
it's fairly easy to get the two different KeyErrors mixed up, at least.
So don't do that, then.
why's a "help us fix bugs related to exception handling" proposal getting so much pushback? I don't understand. this is a bigger problem in the context of massive frameworks, where you can have layers upon layers of exception handling for all sorts of different stuff. everything from catching KeyError to skip missing entries in a dictionary, in template engines, to... honestly idk what else those massive frameworks like to skip, but I regularly see ppl complaining that their framework's exception handling makes their code undebuggable. and I've been paying attention to python's IRC for years. it's a real problem. I can't be the only person who has noticed this. sure, these language changes wouldn't magically solve that problem. but currently you can't solve the problem *at all* because there's no way to tell anything apart. the language changes would just be a step in the right direction. opinions? bikesheds? bring it forward. don't just keep it to yourself.
On 11/04/20 12:56 am, Soni L. wrote:
why's a "help us fix bugs related to exception handling" proposal getting so much pushback? I don't understand.
You're proposing a syntax change, which is a *very* big deal. It requires a substantial amount of justification.
idk what else those massive frameworks like to skip, but I regularly see ppl complaining that their framework's exception handling makes their code undebuggable.
Maybe you move in different circles from me. I haven't seen large numbers of complaints like this. Can you provide some specific examples of these frameworks and the problems people are having with them?
but currently you can't solve the problem *at all*
I wouldn't say that -- there are ways of minimising these kinds of problems, as I talked about in my previous message. -- Greg
On Apr 10, 2020, at 06:00, Soni L. <fakedme+py@gmail.com> wrote
why's a "help us fix bugs related to exception handling" proposal getting so much pushback? I don't understand.
Because it’s a proposal for a significant change to the language semantics that includes a change to the syntax, which is a very high bar to pass. Even for smaller changes that can be done purely in the library, the presumption is always conservative, but the higher the bar, the more pushback. There are also ways your proposal could be better. You don’t have a specific real life example. Your toy example doesn’t look like a real problem, and the fix makes it less readable and less pythonic. Your general rationale is that it won’t fix anything but it might make it possible for frameworks to fix problems that you insist exist but haven’t shown us—which is not a matter of “why should anyone trust you that they exist?”, but of “how can anyone evaluate how good the fix is without seeing them?” But most of this is stuff you could solve now, by answering the questions people are asking you. Sure, some of it is stuff you could have anticipated and answered preemptively, but even a perfectly thought-out and perfectly formed proposal will get pushback; it’s just more likely to survive it. If you’re worried that it’s personal, that people are pushing back because it comes from you and you’ve recently proposed a whole slew of radical half-baked ideas that all failed to get very far, or that your tone doesn’t fit the style or the Python community, or whatever, I don’t think so. Look at the proposal to change variable deletion time—that’s gotten a ton of pushback, and it’s certainly not because nobody respects Guido or nobody likes him.
On 2020-04-10 2:24 p.m., Andrew Barnert wrote:
On Apr 10, 2020, at 06:00, Soni L. <fakedme+py@gmail.com> wrote
why's a "help us fix bugs related to exception handling" proposal getting so much pushback? I don't understand.
Because it’s a proposal for a significant change to the language semantics that includes a change to the syntax, which is a very high bar to pass. Even for smaller changes that can be done purely in the library, the presumption is always conservative, but the higher the bar, the more pushback.
There are also ways your proposal could be better. You don’t have a specific real life example. Your toy example doesn’t look like a real problem, and the fix makes it less readable and less pythonic. Your general rationale is that it won’t fix anything but it might make it possible for frameworks to fix problems that you insist exist but haven’t shown us—which is not a matter of “why should anyone trust you that they exist?”, but of “how can anyone evaluate how good the fix is without seeing them?” But most of this is stuff you could solve now, by answering the questions people are asking you. Sure, some of it is stuff you could have anticipated and answered preemptively, but even a perfectly thought-out and perfectly formed proposal will get pushback; it’s just more likely to survive it.
If you’re worried that it’s personal, that people are pushing back because it comes from you and you’ve recently proposed a whole slew of radical half-baked ideas that all failed to get very far, or that your tone doesn’t fit the style or the Python community, or whatever, I don’t think so. Look at the proposal to change variable deletion time—that’s gotten a ton of pushback, and it’s certainly not because nobody respects Guido or nobody likes him.
hm. okay. so, for starters, here's everything I'm worried about. in one of my libraries (yes this is real code. all of this is taken from stuff I'm deploying.) I have the following piece of code: def _extract(self, obj): try: yield (self.key, obj[self.key]) except (TypeError, IndexError, KeyError): if not self.skippable: raise exceptions.ValidationError (A Boneless Datastructure Language :: abdl._vm:84-89, AGPLv3-licensed, https://ganarchy.autistic.space/project/0f74bd87a23b515b45da7e6f5d9cc8238044... @ 34551d96ce021d2264094a4941ef15a64224d195) this library handles all sorts of arbitrary objects - dicts, sets, lists, defaultdicts, wrappers that are registered with collections.abc.Sequence/Mapping/Set, self-referential data structures, and whatnot. (and btw can we get the ability to index into a set to get the originally inserted element yet) - which means I need to treat all sorts of potential errors as errors. however, sometimes those aren't errors, but intended flow control, such as when your program's config has an integer list in the "username" field. in that case, I raise a ValidationError, and you handle it, and we're all good. (or sometimes you want to skip that entry altogether but anyway.) due to the wide range of supported objects, I can't expect the TypeError to always come from my attempt to index into a set, or the IndexError to always come from my attempt to index into a sequence, or the KeyError to always come from my attempt to index into a mapping. those could very well be coming from a bug in someone's weird sequence/mapping/set implementation. I have no way of knowing! I also don't have a good way of changing this to wrap stuff in RuntimeError, unfortunately. (and yes, this can be mitigated by encouraging the library user to write unit tests and integration tests and whatnot... which is easier said than done. and that won't necessarily catch these bugs, either. (ugh so many times I've had to debug ABDL just going into an infinite loop somewhere because I got the parser wrong >.< unit tests didn't help me there, but anyway...)) "exception spaces" would enable me to say "I want your (operator/function/whatnot) to raise some errors in my space, so I don't confuse them with bugs in your space instead". and they'd get me exactly that. it's basically a hybrid of exceptions and explicit error handling. all the drawbacks of exceptions, with all the benefits of explicit error handling. which does make it worse than both tbh. it's also backwards compatible. I'm trying to come up with a way to explain how "exception spaces" relate to things like rust's .unwrap() on an Result::Err, or nesting a Result in a Result so the caller has to deal with it instead of you, or whatnot, but uh this is surprisingly difficult without mentioning rust code. but think of this like inverted rust errors - while in rust you handle the errors in a return value, with my proposal you'd handle the errors by passing in an argument. or a global. or hidden state. anyway, this is unfortunately more powerful. my "toy example" (the one involving my use-case, not the one trying to define the semantics of these "exception spaces") is also real code. (GAnarchy :: ganarchy.config:183-201, AGPLv3-licensed, https://ganarchy.autistic.space/project/385e734a52e13949a7a5c71827f6de920dbf... @ not yet committed) it's just... it doesn't quite hit this issue like ABDL, template engines, and other things doing more complex things do. I'm sorry I don't have better examples, but this isn't the first time I worry my code is gonna mask bugs. it's not gonna be the last, either. anyway, I'm gonna keep pushing for this because it's probably the easiest way to retrofix explicit error handling into python, while not being as ugly and limiting as wrapping everything in RuntimeError like I proposed previously. (that *was* a bad proposal, tbh. sorry.) I'll do my best to keep adding more and more real code to this thread showing examples where current exception handling isn't quite good enough and risks masking bugs, as I notice them. which probably means only my own code, but oh well.
On Sat, Apr 11, 2020 at 4:35 AM Soni L. <fakedme+py@gmail.com> wrote:
in one of my libraries (yes this is real code. all of this is taken from stuff I'm deploying.) I have the following piece of code:
def _extract(self, obj): try: yield (self.key, obj[self.key]) except (TypeError, IndexError, KeyError): if not self.skippable: raise exceptions.ValidationError
this library handles all sorts of arbitrary objects - dicts, sets, lists, defaultdicts, wrappers that are registered with collections.abc.Sequence/Mapping/Set, self-referential data structures, and whatnot. (and btw can we get the ability to index into a set to get the originally inserted element yet) - which means I need to treat all sorts of potential errors as errors. however, sometimes those aren't errors, but intended flow control, such as when your program's config has an integer list in the "username" field. in that case, I raise a ValidationError, and you handle it, and we're all good. (or sometimes you want to skip that entry altogether but anyway.)
due to the wide range of supported objects, I can't expect the TypeError to always come from my attempt to index into a set, or the IndexError to always come from my attempt to index into a sequence, or the KeyError to always come from my attempt to index into a mapping. those could very well be coming from a bug in someone's weird sequence/mapping/set implementation. I have no way of knowing!
If someone's __getitem__ raises LookupError (which covers both Index and Key), you should assume that the key wasn't found. That is exactly what the protocol is. If that exception was the result of a bug, that is no different from it unexpectedly returning None when it should have returned a value - it's a bug, and it's not up to you to try to handle it. TypeError is a bit less obvious, but if this is really an issue, just special-case sets, and have done with it. Don't try to protect against everyone else's theoretically-possible bugs. You can never catch them all anyway. At some point, you just have to let the blame land where it belongs, and let your code fail if the code it's calling on is buggy. What you're suggesting here is on par with saying "but what if someone uploads a newer version of a package to PyPI and gives it a lower version number?". It's not pip's problem to fix that, and it's not pip's fault if it gets things wrong. ChrisA
On 2020-04-10 3:43 p.m., Chris Angelico wrote:
On Sat, Apr 11, 2020 at 4:35 AM Soni L. <fakedme+py@gmail.com> wrote:
in one of my libraries (yes this is real code. all of this is taken from stuff I'm deploying.) I have the following piece of code:
def _extract(self, obj): try: yield (self.key, obj[self.key]) except (TypeError, IndexError, KeyError): if not self.skippable: raise exceptions.ValidationError
this library handles all sorts of arbitrary objects - dicts, sets, lists, defaultdicts, wrappers that are registered with collections.abc.Sequence/Mapping/Set, self-referential data structures, and whatnot. (and btw can we get the ability to index into a set to get the originally inserted element yet) - which means I need to treat all sorts of potential errors as errors. however, sometimes those aren't errors, but intended flow control, such as when your program's config has an integer list in the "username" field. in that case, I raise a ValidationError, and you handle it, and we're all good. (or sometimes you want to skip that entry altogether but anyway.)
due to the wide range of supported objects, I can't expect the TypeError to always come from my attempt to index into a set, or the IndexError to always come from my attempt to index into a sequence, or the KeyError to always come from my attempt to index into a mapping. those could very well be coming from a bug in someone's weird sequence/mapping/set implementation. I have no way of knowing!
If someone's __getitem__ raises LookupError (which covers both Index and Key), you should assume that the key wasn't found. That is exactly what the protocol is. If that exception was the result of a bug, that is no different from it unexpectedly returning None when it should have returned a value - it's a bug, and it's not up to you to try to handle it.
TypeError is a bit less obvious, but if this is really an issue, just special-case sets, and have done with it.
Don't try to protect against everyone else's theoretically-possible bugs. You can never catch them all anyway. At some point, you just have to let the blame land where it belongs, and let your code fail if the code it's calling on is buggy. What you're suggesting here is on par with saying "but what if someone uploads a newer version of a package to PyPI and gives it a lower version number?". It's not pip's problem to fix that, and it's not pip's fault if it gets things wrong.
rust deals with this issue cleanly and effectively. we should do similarly. ofc, we have exceptions, not return values, so we need a solution designed with exceptions in mind.
ChrisA _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/RNBR75... Code of Conduct: http://python.org/psf/codeofconduct/
On 11/04/20 6:34 am, Soni L. wrote:
def _extract(self, obj): try: yield (self.key, obj[self.key]) except (TypeError, IndexError, KeyError): if not self.skippable: raise exceptions.ValidationError
You can separate out the TypeError like this: try: get = obj.__getitem__ except TypeError: ... try: yield (self.key, get(self.key)) except (IndexError, KeyError): ...
I also don't have a good way of changing this to wrap stuff in RuntimeError
Your proposed solution also requires everyone to update their __getitem__ methods before it will work. What's more, in the transition period (which you can expect to be *very* long) when not everyone has done so, your code would fail much of the time, because you would only be catching exceptions that were raised "in" the appropriate object, and would miss anything raised by old methods that did not use "in". So your solution kind of has a chicken-and-egg problem. It wouldn't work unless everyone started using it everywhere at the same time, which is never going to happen. -- Greg
On 2020-04-10 11:16 p.m., Greg Ewing wrote:
On 11/04/20 6:34 am, Soni L. wrote:
def _extract(self, obj): try: yield (self.key, obj[self.key]) except (TypeError, IndexError, KeyError): if not self.skippable: raise exceptions.ValidationError
You can separate out the TypeError like this:
try: get = obj.__getitem__ except TypeError: ... try: yield (self.key, get(self.key)) except (IndexError, KeyError): ...
I also don't have a good way of changing this to wrap stuff in RuntimeError
Your proposed solution also requires everyone to update their __getitem__ methods before it will work. What's more, in the transition period (which you can expect to be *very* long) when not everyone has done so, your code would fail much of the time, because you would only be catching exceptions that were raised "in" the appropriate object, and would miss anything raised by old methods that did not use "in".
So your solution kind of has a chicken-and-egg problem. It wouldn't work unless everyone started using it everywhere at the same time, which is never going to happen.
They used to say that about Rust.
On Sat, Apr 11, 2020 at 12:26 PM Soni L. <fakedme+py@gmail.com> wrote:
They used to say that about Rust.
Rust prides itself on not having exception handling, forcing everyone to do explicit return value checking. I'm not sure how this factors into the current discussion, since it forces all these functions to return two values all the time (an actual return value and an OK/Error status), which you can quite happily do in Python if you so choose, and in fact has already been mentioned here. It's not an improvement over exception handling - it's just a different way of spelling it (and one that forces you to handle exceptions immediately and reraise them every level explicitly). ChrisA
On Apr 10, 2020, at 19:46, Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Apr 11, 2020 at 12:26 PM Soni L. <fakedme+py@gmail.com> wrote:
They used to say that about Rust.
Rust prides itself on not having exception handling, forcing everyone to do explicit return value checking. I'm not sure how this factors into the current discussion, since it forces all these functions to return two values all the time (an actual return value and an OK/Error status),
That’s not quite true. A Result is effectively a ragged union of a value or an error; you don’t explicitly specify a return value plus an OK or an Error, you specify either an Ok(return value) or an Err(error value). Sure, technically that’s the same amount of information, but the way it’s done, together with the syntactic sugar provided by the language, makes a big difference, which I’ll get to below.
which you can quite happily do in Python if you so choose,
Now, this is the key point. You can do this in Python, but it won’t work like it does in Rust. It’s not just that you won’t get the compiler verification that you covered all code paths (it might be possible to come up with types that mypy can verify here; I haven’t tried…); it’s that your code will look like 2 lines of boilerplate for every line of code, and still won’t interoperate well with the stdlib or the vast majority of third-party libraries unless you explicitly wrap the hell out of everything. There are multiple error handling schemes that can be made to work well—but as far as I know, nobody has discovered one that can be made to work well if it isn’t used ubiquitously. Python, Rust, Scala, Swift, C#, Haskell, etc. are all good languages to deal with errors in—they all picked one, implemented it well, and strongly encourage everyone to use it everywhere. Meanwhile, C++ has three different error handling schemes, two of which are implemented very nicely, but it’s a nightmare to use, because every complex C++ program has to deal with all three of them and map back and forth all over the place. An even more relevant illustration is JavaScript async code. Before there was a consistent convention it was next to impossible to use different libraries together without frequent bugs of the “callback never got called because we got lost somewhere in the chain and there’s no way to tell where” variety, and that couldn’t be fixed by just the language, it had to be fixed by every single popular library being rewritten or replaced by a new library. And that’s the problem with Soni’s proposal. It’s a good system, and designing a new language around it and letting an ecosystem build up around that language might well give you something better than Python. But that doesn’t mean changing the language now will give us something better. That only worked in JS because the status quo ante was so terrible and the benefits so big (not to mention Node coming along and completely changing the target surface for the language toward the end of the transition). But here, the status quo ante is fine for most people most of the time, and the benefits small, so we’re never going to get the whole ecosystem updated or replaced, so we’re never going to get the advantages.
and in fact has already been mentioned here. It's not an improvement over exception handling - it's just a different way of spelling it (and one that forces you to handle exceptions immediately and reraise them every level explicitly).
No it doesn’t. That _is_ true in Go, which gives us a perfect opportunity to compare. Let’s take something non-trivial but pretty simple and compare Pythonesque pseudocode for how you’d write it in different languages: Python: val = func() val = func2(val) try: val = func3(val) except Error3: return defval return func4(val) Rust: val = func()? val = func2(val)? val = match func3(val): Ok(v): v Err(v): return Ok(defval) return func4(val) Go: val, err = func() if err: return None, err val, err = func2(val) if err: return None, err val, err = func3(val) if err: return default, None val, err = func4(val) if err: return None, err return val, None Go does force you to explicitly handle and reraise every error, which not only drowns your normal flow in boilerplate, it makes it hard to notice your actual local error handling because it looks almost the same as the boilerplate. But Rust lets you bubble up errors unobtrusively, and your local error handling is immediately visible, just like Python. (I know this is a silly toy example, but read actual published code in all three languages, and it really does pan out like this.) The Rust argument is that this isn’t just as good as Python, it’s better, ultimately because it’s easier for a compiler to verify and to optimize—but those are considerations that aren’t normally top priority in Python, and arguably wouldn’t be achievable in any realistic Python implementation anyway. On the other hand, Rust has less runtime information and dynamic control in the rare cases where you need to do tricky stuff, which are considerations that aren’t normally top priority in Rust, and arguably wouldn’t be achievable in any realistic Rust implementation anyway. But for the vast majority of code, they’re both great.
Soni L. writes:
so, for starters, here's everything I'm worried about.
in one of my libraries (yes this is real code. all of this is taken from stuff I'm deploying.) I have the following piece of code:
def _extract(self, obj): try: yield (self.key, obj[self.key]) except (TypeError, IndexError, KeyError): if not self.skippable: raise exceptions.ValidationError
due to the wide range of supported objects, I can't expect the TypeError to always come from my attempt to index into a set, or the IndexError to always come from my attempt to index into a sequence, or the KeyError to always come from my attempt to index into a mapping. those could very well be coming from a bug in someone's weird sequence/mapping/set implementation. I have no way of knowing!
I hope that exclamation point doesn't mean you think anybody doesn't understand the problem by now. I think most serious programmers have run into "expected" Exceptions masking unexpected errors. What we don't understand is (a) how your proposed fix is supposed to work without a tremendous effort by third parties (and Python itself in the stdlib), (b) if you do presume enormous amounts of effort, what is in it for the third parties, and (c) why doesn't asking the third parties to create finely graduated hierarchies of exceptions do as well (if not better) than the syntax change? The grungy details: So as far as I can tell, your analysis above is exactly right. You have no way of knowing, and with the syntax change *you* still will have no way of knowing. You will be dependent on the implementer to tell you, ie, the obj's getter must distinguish between an actual "this key is not in the object" Error, and an implementation bug. You're asking everybody else to consistently distinguish between KeyErrors you think are KeyErrors and KeyErrors you think are actually bugs. I don't see how they can know what you think, and of course if you're right that they are actually bugs, by that very token the relevant developer probably doesn't know they are there! And I don't think they'll care enough to make the effort even if they are aware of the difference. Because, if they did care, they already can make the effort by using a wider variety of builtin exceptions, or by adding attributes to the exceptions they raise, or deriving subclasses of the various exceptions that might be raised, or of RuntimeError (and they might disagree on what exception *should* be the base exception!) But they don't do any of those very often. And I've seen principled objections to the proliferation of specialized exceptions (sorry, no cite; ignore if you like). I don't recall them clearly but I suppose some of them would apply to this syntax.
"exception spaces" would enable me to say "I want your (operator/function/whatnot) to raise some errors in my space, so I don't confuse them with bugs in your space instead". and they'd get me exactly that.
But how do you say that to me? I'm not your subcontractor, I'm an independent developer. I write a library, I *don't know* what "your" exception spaces are. It's the other way around, I guess? That is, I raise exceptions in *my* space and some in builtin space and maybe even some in imported spaces and you have to catch them explicitly to distinguish. I think you're going to have to do a lot of studying and a lot of extra work on exception handling to catch only the exceptions you intend to. I'm not sure it's going to be worth it to do the necessary research to be able to write # these are KeyErrors you deem to be the real McCoy except KeyError in SpaceX | SpaceY | SpaceZ: handle_spacey_errors() # these are exceptions from spaces that you think are probably bugs except KeyError: raise OutDamnSpotError whenever you're handling potential exceptions from third-party code. And I think you have to do it consistently, because otherwise you may be missing lots of masked bugs. If it's not worth it for *you* to use it *consistently*, why would the tremendous effort imposed on others to lay the foundations for your "occasional" use be worth it? One of the main points of exceptions as a mechanism is that they "bubble up" until caught. If they occurred deep in the import hierarchy, you may consider that a bug if middle layers don't distinguish, but the developer-in-the-middle may disagree with you if you're handing their library inputs they never intended to handle. I guarantee you lose that argument, unless you are *very* nice to Ms. DITM. Bottom line: I just don't see how this can work in practice unless we make the "in" clause mandatory, which I suspect is a Python 4.0 compatibility break of Python 3.0 annoyance. I know it would annoy me: my exception handling is almost all of the "terminate processing, discard pending input as possibly corrupt, reinitialize, restart processing" variety. I just don't care about "masked bugs".
it's also backwards compatible.
I don't think so, if it's to be useful to you or anyone else. See discussion preceding "unless we make it mandatory," above.
anyway, I'm gonna keep pushing for this because it's probably the easiest way to retrofix explicit error handling into python,
You're not going to get that, I'm pretty sure, except to the extent that you already have it in the form of Exception subclasses. Python is not intended to make defects hard to inject the way Rust is. (Those are observations, not policy statements. I don't have much if any influence over policy. ;-) I just know a lot about incentives in my professional capacity.) Steve
On 2020-04-11 5:40 a.m., Stephen J. Turnbull wrote:
Soni L. writes:
so, for starters, here's everything I'm worried about.
in one of my libraries (yes this is real code. all of this is taken from stuff I'm deploying.) I have the following piece of code:
def _extract(self, obj): try: yield (self.key, obj[self.key]) except (TypeError, IndexError, KeyError): if not self.skippable: raise exceptions.ValidationError
due to the wide range of supported objects, I can't expect the TypeError to always come from my attempt to index into a set, or the IndexError to always come from my attempt to index into a sequence, or the KeyError to always come from my attempt to index into a mapping. those could very well be coming from a bug in someone's weird sequence/mapping/set implementation. I have no way of knowing!
I hope that exclamation point doesn't mean you think anybody doesn't understand the problem by now. I think most serious programmers have run into "expected" Exceptions masking unexpected errors. What we don't understand is (a) how your proposed fix is supposed to work without a tremendous effort by third parties (and Python itself in the stdlib), (b) if you do presume enormous amounts of effort, what is in it for the third parties, and (c) why doesn't asking the third parties to create finely graduated hierarchies of exceptions do as well (if not better) than the syntax change?
The grungy details:
So as far as I can tell, your analysis above is exactly right. You have no way of knowing, and with the syntax change *you* still will have no way of knowing. You will be dependent on the implementer to tell you, ie, the obj's getter must distinguish between an actual "this key is not in the object" Error, and an implementation bug. You're asking everybody else to consistently distinguish between KeyErrors you think are KeyErrors and KeyErrors you think are actually bugs. I don't see how they can know what you think, and of course if you're right that they are actually bugs, by that very token the relevant developer probably doesn't know they are there!
And I don't think they'll care enough to make the effort even if they are aware of the difference. Because, if they did care, they already can make the effort by using a wider variety of builtin exceptions, or by adding attributes to the exceptions they raise, or deriving subclasses of the various exceptions that might be raised, or of RuntimeError (and they might disagree on what exception *should* be the base exception!) But they don't do any of those very often. And I've seen principled objections to the proliferation of specialized exceptions (sorry, no cite; ignore if you like). I don't recall them clearly but I suppose some of them would apply to this syntax.
"exception spaces" would enable me to say "I want your (operator/function/whatnot) to raise some errors in my space, so I don't confuse them with bugs in your space instead". and they'd get me exactly that.
But how do you say that to me? I'm not your subcontractor, I'm an independent developer. I write a library, I *don't know* what "your" exception spaces are. It's the other way around, I guess? That is, I raise exceptions in *my* space and some in builtin space and maybe even some in imported spaces and you have to catch them explicitly to distinguish. I think you're going to have to do a lot of studying and a lot of extra work on exception handling to catch only the exceptions you intend to. I'm not sure it's going to be worth it to do the necessary research to be able to write
# these are KeyErrors you deem to be the real McCoy except KeyError in SpaceX | SpaceY | SpaceZ: handle_spacey_errors()
# these are exceptions from spaces that you think are probably bugs except KeyError: raise OutDamnSpotError
whenever you're handling potential exceptions from third-party code. And I think you have to do it consistently, because otherwise you may be missing lots of masked bugs. If it's not worth it for *you* to use it *consistently*, why would the tremendous effort imposed on others to lay the foundations for your "occasional" use be worth it?
One of the main points of exceptions as a mechanism is that they "bubble up" until caught. If they occurred deep in the import hierarchy, you may consider that a bug if middle layers don't distinguish, but the developer-in-the-middle may disagree with you if you're handing their library inputs they never intended to handle. I guarantee you lose that argument, unless you are *very* nice to Ms. DITM.
Bottom line: I just don't see how this can work in practice unless we make the "in" clause mandatory, which I suspect is a Python 4.0 compatibility break of Python 3.0 annoyance. I know it would annoy me: my exception handling is almost all of the "terminate processing, discard pending input as possibly corrupt, reinitialize, restart processing" variety. I just don't care about "masked bugs".
for starters: operator variants wherever exceptions are used as part of API contract, and add a kwarg to functions that explicitly define exceptions as part of their API contract. if missing, defaults to None. all exceptions not raised in a specific channel will not match a specific channel, but will match None. so e.g. you do: def foo(espace=None): if thing(): raise Bar in espace which means if the API user isn't expecting to have to handle an error, they can just do: x = foo() and then they won't handle it. but if they want to handle it, they can do: try: x = foo(espace=my_espace) except Bar in my_espace: ... and then they'll handle it. alternatively, they can let their caller handle it by having the espace come from the caller. def bar(espace=None): x = foo(espace=espace) and then the caller is expected to handle it, *if they so choose*. by default the None handler works even for exceptions in a channel: try: x = foo(espace=NotNone) except Bar: ... this'll also catch it. this is for backwards compatibility so existing still code catches exceptions without caring where they come from, and also so you can still have a "catches everything" construct. we'll also need operator variants. I was thinking of the following: x.in foo bar # for attribute access x[in foo bar] # for item access x +in foo bar # for math # etc (yes, these are very ugly. this is one of the unfortunate things of trying to retrofit this into an existing language. but they're more likely to catch bugs so I'm not worried.) which would call new methods that take in an espace. (we could retrofit existing methods with an optional espace but then we lose the ability to catch everything and shove it in the appropriate espace anyway. this would significantly reduce the effort required to make the stdlib work with this new thing.) maybe this should be python 4. I don't think any language's ever done Rust-like explicit error handling (i.e. avoiding the issue of swallowing bugs) using exceptions before. it could even be sugar for "except Foo: if Foo.__channel__ != channel: raise" or something, I guess. but... it's definitely worth it. and I don't think it'll be a python 3-level annoyance, if implemented like this. .
it's also backwards compatible.
I don't think so, if it's to be useful to you or anyone else. See discussion preceding "unless we make it mandatory," above.
anyway, I'm gonna keep pushing for this because it's probably the easiest way to retrofix explicit error handling into python,
You're not going to get that, I'm pretty sure, except to the extent that you already have it in the form of Exception subclasses. Python is not intended to make defects hard to inject the way Rust is. (Those are observations, not policy statements. I don't have much if any influence over policy. ;-) I just know a lot about incentives in my professional capacity.)
Steve
On Sat, Apr 11, 2020 at 9:30 PM Soni L. <fakedme+py@gmail.com> wrote:
alternatively, they can let their caller handle it by having the espace come from the caller.
def bar(espace=None): x = foo(espace=espace)
and then the caller is expected to handle it, *if they so choose*.
If a function just replicates down the espace parameter, how is that even slightly different from the current situation? You STILL won't be able to know where the exception came from. I am at a complete loss to understand how all of this overhead (and there is a LOT of overhead) even solves the problem. And if a function doesn't replicate it down, what exception space should it be using? How would it know?
we'll also need operator variants. I was thinking of the following:
x.in foo bar # for attribute access x[in foo bar] # for item access x +in foo bar # for math # etc
(yes, these are very ugly. this is one of the unfortunate things of trying to retrofit this into an existing language. but they're more likely to catch bugs so I'm not worried.)
I don't think they ARE more likely to catch bugs. You're making an incredibly ugly system that still has no indication that it'll help.
maybe this should be python 4. I don't think any language's ever done Rust-like explicit error handling (i.e. avoiding the issue of swallowing bugs) using exceptions before.
That'd be because mandatory explicit error handling is identical to mandatory return value checking, which is what you get in C and plenty of other languages. Exceptions don't give you any benefit if you have to check for them after every single function call. ChrisA
On 2020-04-11 8:38 a.m., Chris Angelico wrote:
On Sat, Apr 11, 2020 at 9:30 PM Soni L. <fakedme+py@gmail.com> wrote:
alternatively, they can let their caller handle it by having the espace come from the caller.
def bar(espace=None): x = foo(espace=espace)
and then the caller is expected to handle it, *if they so choose*.
If a function just replicates down the espace parameter, how is that even slightly different from the current situation? You STILL won't be able to know where the exception came from. I am at a complete loss to understand how all of this overhead (and there is a LOT of overhead) even solves the problem.
And if a function doesn't replicate it down, what exception space should it be using? How would it know?
That just means you completely misunderstood the proposal. None would still work (and be the default). raising things in None would be the same as using python today. the special things happen when you don't raise in None. yeah sure your function has an espace but if you don't use it... the caller won't catch those errors. def bar(espace=None): x = foo(espace=espace) y = x['bug'] if called with an espace other than None, the explicitly declared errors from foo will be raised in the given espace. but the indexing in x['bug']? that'll just give you a KeyError in None, indicating there's a bug in bar. you can do this. you can do all sorts of weird things. you don't have to immediately handle them. you can let them bubble up. but espaces/channels (I've been calling them both) let you be explicit about which raisers and which handlers link up to eachother. you aren't supposed to mindlessly pass espaces forward. if your function isn't supposed to raise, *don't use espaces* and don't use the new operators. that's perfectly valid, and is in fact intended. if you decide that a future version needs to be able to raise, you can add espaces then. but you wouldn't litter your codebase with espaces: you'd have an espace=None argument, and perhaps one or two statements where you pass in an espace (either with espace=espace or espace-aware operators). or you might not even accept an espace and instead just want to handle errors in the function itself but don't wanna mask bugs. all this is valid, and intended.
we'll also need operator variants. I was thinking of the following:
x.in foo bar # for attribute access x[in foo bar] # for item access x +in foo bar # for math # etc
(yes, these are very ugly. this is one of the unfortunate things of trying to retrofit this into an existing language. but they're more likely to catch bugs so I'm not worried.)
I don't think they ARE more likely to catch bugs. You're making an incredibly ugly system that still has no indication that it'll help.
maybe this should be python 4. I don't think any language's ever done Rust-like explicit error handling (i.e. avoiding the issue of swallowing bugs) using exceptions before.
That'd be because mandatory explicit error handling is identical to mandatory return value checking, which is what you get in C and plenty of other languages. Exceptions don't give you any benefit if you have to check for them after every single function call.
ChrisA _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/57Y6LD... Code of Conduct: http://python.org/psf/codeofconduct/
tl;dr: I show how the goal of Soni L's exception spaces can be addressed today, via less intrusive means. (Assuming I understand their proposal, that is.) On 4/11/2020 7:49 AM, Soni L. wrote:
On 2020-04-11 8:38 a.m., Chris Angelico wrote:
On Sat, Apr 11, 2020 at 9:30 PM Soni L. <fakedme+py@gmail.com> wrote:
alternatively, they can let their caller handle it by having the espace come from the caller.
def bar(espace=None): x = foo(espace=espace)
and then the caller is expected to handle it, *if they so choose*.
If a function just replicates down the espace parameter, how is that even slightly different from the current situation? You STILL won't be able to know where the exception came from. I am at a complete loss to understand how all of this overhead (and there is a LOT of overhead) even solves the problem.
And if a function doesn't replicate it down, what exception space should it be using? How would it know?
That just means you completely misunderstood the proposal. None would still work (and be the default). raising things in None would be the same as using python today.
the special things happen when you don't raise in None. yeah sure your function has an espace but if you don't use it... the caller won't catch those errors.
def bar(espace=None): x = foo(espace=espace) y = x['bug']
if called with an espace other than None, the explicitly declared errors from foo will be raised in the given espace. but the indexing in x['bug']? that'll just give you a KeyError in None, indicating there's a bug in bar.
In your proposal, you're requiring all library code that wants to use exception spaces to be modified to accept your exception space parameter, and then to change where they raise exception space exceptions. Is that correct? I'm assuming so for this discussion. Further, client code that wants to participate in these exception spaces also need to change how they call the library code and how they catch exceptions. As others have suggested, this can be achieved today with less disruption just by subclassing exceptions in the library. Consider a library that exports a single function that might raise a KeyError, and that's defined as part of its interface. In my example below, it will always raise that error. Rather than change the function to take a new parameter and change how it raises exceptions, it could work as follows: ##################### # library code class LibExceptionBase(Exception): pass class LibKeyError(LibExceptionBase, KeyError): pass # Other library API exceptions. class LibValueError(LibExceptionBase, ValueError): pass def lib_fn(a): raise LibKeyError(f'{a!r} not found') ##################### # User code mymap = {'x': 4} # Case 0: I have a "local" problem in calling the library, which raises # KeyError. This would be similar to your code which passes in an espace. try: lib_fn(mymap['bad key']) except LibExceptionBase as ex: print('0 library exception', ex) except KeyError as ex: print('0 normal exception', ex) # Case 1: I call the library, which raises LibKeyError. This would be similar # to your code which passes in an espace. try: lib_fn(mymap['x']) except LibExceptionBase as ex: print('1 library exception', ex) except KeyError as ex: print('1 normal exception', ex) # Case 2: Existing code that doesn't know about exception spaces, and has a # "local" problem that raises KeyError. This would be code that passes in # espace=None (via the default) in your examples. try: lib_fn(mymap['bad key']) except KeyError as ex: print('2 Some KeyError', ex) # Case 3: Existing code that doesn't know about exception spaces, and the # library raises its a KeyErorr. This would be code that passes in espace=None # (via the default) in your examples. try: lib_fn(4) except KeyError as ex: print('3 Some KeyError', ex) ##################### This produces: 0 normal exception 'bad key' 1 library exception '4 not found' 2 Some KeyError 'bad key' 3 Some KeyError '4 not found' In cases 2 and 3 there likely wouldn't be an exception handler, and the error would just propagate. I'm catching it here just to show that it's a KeyError that's propagating, although in case 3 it's really a LibKeyError (a subclass of KeyError). In my example, the library code that wants to use exception spaces needs to change (same as your proposal), the client code that wants to use exception spaces needs to change (same as your proposal), and the client code that doesn't want to exception spaces doesn't need to change (same as your proposal). In my case, the library changes involve defining exception classes and require the library to catch and re-throw various exceptions. In your proposal, the library changes involve new function parameters and require the library to catch and re-throw exceptions in a new way. In my example, notice that the user code that cares about exceptions spaces just needs to catch the library-specific exception base class. Or it could go further by catching a specific library exception. And the user code that doesn't care about exception spaces requires no changes. In your proposal, the user code that does care would need to change how the functions are called, and would need to change how they catch exceptions. The code that doesn't care about exception spaces requires no changes. I think my example and your proposal achieve the same effect, with a similar amount of required changes on the library side and on client code that wants to participate. Both my example and your proposal require no changes to client code that doesn't want to participate. The difference is that my example requires no changes to Python and so works today. It's also probably more efficient because it doesn't require the extra espace parameter.
you can do this. you can do all sorts of weird things. you don't have to immediately handle them. you can let them bubble up. but espaces/channels (I've been calling them both) let you be explicit about which raisers and which handlers link up to eachother. you aren't supposed to mindlessly pass espaces forward. if your function isn't supposed to raise, *don't use espaces* and don't use the new operators. that's perfectly valid, and is in fact intended. if you decide that a future version needs to be able to raise, you can add espaces then. but you wouldn't litter your codebase with espaces: you'd have an espace=None argument, and perhaps one or two statements where you pass in an espace (either with espace=espace or espace-aware operators). or you might not even accept an espace and instead just want to handle errors in the function itself but don't wanna mask bugs. all this is valid, and intended.
The problem with "if you decide that a future version needs to be able to raise" is that you now need to pass an espace parameter all through your code, and into the library. That might be many layers of functions that now need to be modified. If A calls B calls C calls D which calls the library, and A decides it wants use an exception space, then in your proposal B, C, and D also need modifying to pass through the espace argument. With normal exceptions (my example) that's not the case: only A needs modifying. And imagine that C and D aren't in your code, but are in second library. With your proposal, you'd need to modify that library too.
we'll also need operator variants. I was thinking of the following:
x.in foo bar # for attribute access x[in foo bar] # for item access x +in foo bar # for math # etc
(yes, these are very ugly. this is one of the unfortunate things of trying to retrofit this into an existing language. but they're more likely to catch bugs so I'm not worried.)
There have been various proposals like this in the past, revolving around "just raise exceptions in my code, not in any code I call". I can't say if this is better than any of the prior ones. Usually the advice is to just limit the scope of your exception handlers. Although that can't catch everything, I think your operator proposal would have the same issues. But that would of course need to be worked out. Eric
On 2020-04-11 10:27 a.m., Eric V. Smith wrote:
tl;dr: I show how the goal of Soni L's exception spaces can be addressed today, via less intrusive means. (Assuming I understand their proposal, that is.)
what's the point of having standard types if you're not supposed to use them because they mask bugs? what if you end up with a nested call to your own library and catch it at the wrong place? (or, indeed, someone else also uses this technique but somewhere along the way a built-in exception is being caught) you're just delaying the issue/shifting it around, instead of solving it once and for all.
On 4/11/2020 9:38 AM, Soni L. wrote:
On 2020-04-11 10:27 a.m., Eric V. Smith wrote:
tl;dr: I show how the goal of Soni L's exception spaces can be addressed today, via less intrusive means. (Assuming I understand their proposal, that is.)
what's the point of having standard types if you're not supposed to use them because they mask bugs?
what if you end up with a nested call to your own library and catch it at the wrong place? (or, indeed, someone else also uses this technique but somewhere along the way a built-in exception is being caught)
you're just delaying the issue/shifting it around, instead of solving it once and for all.
You show no examples where your proposal addresses those issues but mine doesn't. Your responses sound like the only thing you're interested in is having your proposal accepted, not in the problem that needs to be solved. The fact that you don't address some of my points (for example: changes to intermediate libraries, which your proposal requires) just reinforces this impression. As such, I'm going to drop out of this discussion. Eric
On 2020-04-11 10:46 a.m., Eric V. Smith wrote:
On 4/11/2020 9:38 AM, Soni L. wrote:
On 2020-04-11 10:27 a.m., Eric V. Smith wrote:
tl;dr: I show how the goal of Soni L's exception spaces can be addressed today, via less intrusive means. (Assuming I understand their proposal, that is.)
what's the point of having standard types if you're not supposed to use them because they mask bugs?
what if you end up with a nested call to your own library and catch it at the wrong place? (or, indeed, someone else also uses this technique but somewhere along the way a built-in exception is being caught)
you're just delaying the issue/shifting it around, instead of solving it once and for all.
You show no examples where your proposal addresses those issues but mine doesn't.
Your responses sound like the only thing you're interested in is having your proposal accepted, not in the problem that needs to be solved. The fact that you don't address some of my points (for example: changes to intermediate libraries, which your proposal requires) just reinforces this impression. As such, I'm going to drop out of this discussion.
yes, I absolutely do want my proposal accepted. I'd think anyone coming up with a proposal would want that. but that's not the point I'm trying to make. in current python, you can't safely handle exceptions, *because* of the remote chance of them being raised *by the wrong thing/for the wrong reason*. rust doesn't have that problem, as it effectively forces you to handle exceptions all the time, and is constantly wrapping and unwrapping exceptions in exceptions and whatnot. I'm not saying rust has the best solution. I don't like how it's so explicit about error handling. sometimes letting it bubble up is great. but rust doesn't have the problem of exception handling being inherently dangerous, context-sensitive, and destructive to application logic. python's exception handling is effectively a form of dynamic scoping, as defined by wikipedia: "In some languages, however, "part of a program" refers to "portion of run time (time period during execution)", and is known as *dynamic scope*.". this is the whole problem.
Eric _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/L5UDJM... Code of Conduct: http://python.org/psf/codeofconduct/
On Sun, Apr 12, 2020 at 1:37 AM Soni L. <fakedme+py@gmail.com> wrote:
in current python, you can't safely handle exceptions, *because* of the remote chance of them being raised *by the wrong thing/for the wrong reason*.
rust doesn't have that problem, as it effectively forces you to handle exceptions all the time, and is constantly wrapping and unwrapping exceptions in exceptions and whatnot.
I'm not saying rust has the best solution.
You can't add 0.5 to a Python integers, because there's a remote chance that you'll get OverflowError trying to convert it to float. Some other language, in which all integers are signed 32-bit values, doesn't have this problem, as it effectively forces you to handle overflows all the time. Is that better than Python?
I don't like how it's so explicit about error handling. sometimes letting it bubble up is great. but rust doesn't have the problem of exception handling being inherently dangerous, context-sensitive, and destructive to application logic. python's exception handling is effectively a form of dynamic scoping, as defined by wikipedia: "In some languages, however, "part of a program" refers to "portion of run time (time period during execution)", and is known as dynamic scope.".
this is the whole problem.
You haven't yet proven that this is a problem. You haven't demonstrated that your solution is, in any way, better than simply subclassing the exceptions. Your proposal requires that every level in the call stack explicitly handle exception scoping and that every exception raising site magically know whether to scope the exception or not; Python *already* allows the raise site to choose to use a subclass of the exception type, which is a far more useful mechanism. Your proposal is heavy on magic and very light on actually-usable recommendations. I'm done here. ChrisA
On 2020-04-11 12:52 p.m., Chris Angelico wrote:
On Sun, Apr 12, 2020 at 1:37 AM Soni L. <fakedme+py@gmail.com> wrote:
in current python, you can't safely handle exceptions, *because* of the remote chance of them being raised *by the wrong thing/for the wrong reason*.
rust doesn't have that problem, as it effectively forces you to handle exceptions all the time, and is constantly wrapping and unwrapping exceptions in exceptions and whatnot.
I'm not saying rust has the best solution.
You can't add 0.5 to a Python integers, because there's a remote chance that you'll get OverflowError trying to convert it to float. Some other language, in which all integers are signed 32-bit values, doesn't have this problem, as it effectively forces you to handle overflows all the time. Is that better than Python?
I don't like how it's so explicit about error handling. sometimes letting it bubble up is great. but rust doesn't have the problem of exception handling being inherently dangerous, context-sensitive, and destructive to application logic. python's exception handling is effectively a form of dynamic scoping, as defined by wikipedia: "In some languages, however, "part of a program" refers to "portion of run time (time period during execution)", and is known as dynamic scope.".
this is the whole problem.
You haven't yet proven that this is a problem. You haven't demonstrated that your solution is, in any way, better than simply subclassing the exceptions. Your proposal requires that every level in the call stack explicitly handle exception scoping and that every exception raising site magically know whether to scope the exception or not; Python *already* allows the raise site to choose to use a subclass of the exception type, which is a far more useful mechanism.
Your proposal is heavy on magic and very light on actually-usable recommendations.
It is literally a problem I'm running into, in my code, today. and not something I can workaround. yes, I can (and do) limit the things I put within the "try". yes, I can (and do) use my own exceptions. yes, I can (and do) write unit tests and integration tests. but those are just mitigations, not proper solutions. they reduce the risk but don't eliminate it. why shouldn't we eliminate that risk?
I'm done here.
ChrisA _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/54EVAE... Code of Conduct: http://python.org/psf/codeofconduct/
On 12/04/20 3:57 am, Soni L. wrote:
but those are just mitigations, not proper solutions. they reduce the risk but don't eliminate it. why shouldn't we eliminate that risk?
The risk could be completely eliminated today if everyone rewrote their api-exception-raising methods to catch and reraise exceptions appropriately. No new language features would be needed, and it could be done using only "standard exceptions", as you put it. If people are unwilling to do that, then they won't be willing to rewrite them to use your new exception-space convention either. So even if your proposal were accepted, it wouldn't achieve what you want -- not for any technical reason, but because of human nature. -- Greg
It sounds like you've entirely missed the point of Python exceptions. You are right that they are a kind of dynamic scope. That's their entire reason for existing, not a bug. In Python, we can handle an exception at the particular scope in which it is most appropriate and useful to handle it, and not worry about it elsewhere. On the very rare occasions where I've stumbled on an exception being raised "for the wrong reason" the solution is ALWAYS either to make try/except surround less, or to subclass exceptions to be more specific. If some library or framework is unwilling to subclass, they will be that much less willing to add special, very fragile, syntax for the same purpose. On Sat, Apr 11, 2020, 11:37 AM Soni L.
I'm not saying rust has the best solution. I don't like how it's so explicit about error handling. sometimes letting it bubble up is great. but rust doesn't have the problem of exception handling being inherently dangerous, context-sensitive, and destructive to application logic. python's exception handling is effectively a form of dynamic scoping, as defined by wikipedia: "In some languages, however, "part of a program" refers to "portion of run time (time period during execution)", and is known as *dynamic scope*.".
On 2020-04-11 1:01 p.m., David Mertz wrote:
It sounds like you've entirely missed the point of Python exceptions. You are right that they are a kind of dynamic scope. That's their entire reason for existing, not a bug.
In Python, we can handle an exception at the particular scope in which it is most appropriate and useful to handle it, and not worry about it elsewhere.
On the very rare occasions where I've stumbled on an exception being raised "for the wrong reason" the solution is ALWAYS either to make try/except surround less, or to subclass exceptions to be more specific.
If some library or framework is unwilling to subclass, they will be that much less willing to add special, very fragile, syntax for the same purpose.
the reason I'm proposing this is that I like standard exception types having well-defined semantics. this "special, very fragile, syntax for the same purpose" doesn't take away from that, and instead just adds to it. it's a way of having multiple, independent, "local" dynamic scopes. instead of just one global dynamic scope. and enables you to freely use standard exception types. if anything it's closer to passing in a namespace with a bunch of standard exception types every time you wanna do stuff. which... I could also get behind tbh. an stdlib addition that dynamically exposes subclasses of the standard exception types, unique to that namespace instance. would still need some way of passing it in to operators and stuff tho.
On Sat, Apr 11, 2020, 11:37 AM Soni L.
I'm not saying rust has the best solution. I don't like how it's so explicit about error handling. sometimes letting it bubble up is great. but rust doesn't have the problem of exception handling being inherently dangerous, context-sensitive, and destructive to application logic. python's exception handling is effectively a form of dynamic scoping, as defined by wikipedia: "In some languages, however, "part of a program" refers to "portion of run time (time period during execution)", and is known as *dynamic scope*.".
If I understand correctly, you want a way for distinguishing between exceptions that were explicitly and intentionally `raise`'ed as part of an API and exceptions that were unintentionally raised due to bugs. So for example: raise ValueError(f'Illegal value for xyz: {xyz}') # part of the API foo['bug'] # this should have been 'bag' so it's a bug In addition to that, the user of the API should be able to decide whether they let the API raise in their *exception space* or not. I think you could realize this in today's Python by using `raise ... from espace` where espace is an instance of a custom exception and then check the resulting exception's `__cause__`. So for example: class ESpace(Exception): pass # I'm the user of an API and I want to distinguish their API errors from bugs. espace = ESpace() try: api_func(espace=espace) except KeyError as err: if err.__cause__ is espace: # it's part of the API pass else: # it's a bug pass And the API functions would have to raise their exceptions explicitly from the provided `espace`: def api_func(espace=None): raise KeyError() from espace # part of the API; sets the exception's __cause__ to `espace` foo['bug'] # here __cause__ will be None, just like if no `espace` had been provided It's probably an abuse of the exception mechanism and also relies on a dunder, but for your own projects it could serve the purpose. On 11.04.20 18:07, Soni L. wrote:
the reason I'm proposing this is that I like standard exception types having well-defined semantics. this "special, very fragile, syntax for the same purpose" doesn't take away from that, and instead just adds to it.
it's a way of having multiple, independent, "local" dynamic scopes. instead of just one global dynamic scope. and enables you to freely use standard exception types.
if anything it's closer to passing in a namespace with a bunch of standard exception types every time you wanna do stuff. which... I could also get behind tbh. an stdlib addition that dynamically exposes subclasses of the standard exception types, unique to that namespace instance. would still need some way of passing it in to operators and stuff tho.
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/HAT3Y7... Code of Conduct: http://python.org/psf/codeofconduct/
On 2020-04-11 8:39 p.m., Dominik Vilsmeier wrote:
If I understand correctly, you want a way for distinguishing between exceptions that were explicitly and intentionally `raise`'ed as part of an API and exceptions that were unintentionally raised due to bugs. So for example:
raise ValueError(f'Illegal value for xyz: {xyz}') # part of the API foo['bug'] # this should have been 'bag' so it's a bug
In addition to that, the user of the API should be able to decide whether they let the API raise in their *exception space* or not.
I think you could realize this in today's Python by using `raise ... from espace` where espace is an instance of a custom exception and then check the resulting exception's `__cause__`. So for example:
class ESpace(Exception): pass
# I'm the user of an API and I want to distinguish their API errors from bugs. espace = ESpace() try: api_func(espace=espace) except KeyError as err: if err.__cause__ is espace: # it's part of the API pass else: # it's a bug pass
And the API functions would have to raise their exceptions explicitly from the provided `espace`:
def api_func(espace=None): raise KeyError() from espace # part of the API; sets the exception's __cause__ to `espace` foo['bug'] # here __cause__ will be None, just like if no `espace` had been provided
It's probably an abuse of the exception mechanism and also relies on a dunder, but for your own projects it could serve the purpose.
I figured something better instead. you can have a class ESpace, but you use it like so: espace = ESpace() try: foo(espace=espace) except espace.module.submodule.Exception: ... e.g. for builtins: espace = ESpace() try: raise espace.ValueError except espace.ValueError: ... and it dynamically creates subclasses of whatever you give it. I'm not sure how doable this is in current python, but it's super close to what I want. so hey if it works well, we can promote it to the stdlib? just need to encourage ppl not to check the type of their espace argument so you can silently swap the external one for the stdlib one and nothing breaks. (still need a better way to pass it into operators but eh)
On 11.04.20 18:07, Soni L. wrote:
the reason I'm proposing this is that I like standard exception types having well-defined semantics. this "special, very fragile, syntax for the same purpose" doesn't take away from that, and instead just adds to it.
it's a way of having multiple, independent, "local" dynamic scopes. instead of just one global dynamic scope. and enables you to freely use standard exception types.
if anything it's closer to passing in a namespace with a bunch of standard exception types every time you wanna do stuff. which... I could also get behind tbh. an stdlib addition that dynamically exposes subclasses of the standard exception types, unique to that namespace instance. would still need some way of passing it in to operators and stuff tho.
_______________________________________________ Python-ideas mailing list --python-ideas@python.org To unsubscribe send an email topython-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived athttps://mail.python.org/archives/list/python-ideas@python.org/message/HAT3Y7... Code of Conduct:http://python.org/psf/codeofconduct/
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/X7YXXE... Code of Conduct: http://python.org/psf/codeofconduct/
On 12.04.20 02:20, Soni L. wrote:
I figured something better instead. you can have a class ESpace, but you use it like so:
espace = ESpace()
try: foo(espace=espace) except espace.module.submodule.Exception: ...
e.g. for builtins:
espace = ESpace()
try: raise espace.ValueError except espace.ValueError: ...
and it dynamically creates subclasses of whatever you give it. I'm not sure how doable this is in current python, but it's super close to what I want. so hey if it works well, we can promote it to the stdlib? just need to encourage ppl not to check the type of their espace argument so you can silently swap the external one for the stdlib one and nothing breaks.
(still need a better way to pass it into operators but eh)
This is possible for example by defining ESpace as a class which returns the corresponding exception subclasses via `__getattr__` and stores them in a cache. In order to work with a default (i.e. if no espace is provided) some base class which delegates attribute lookups directly to `builtins` would be required. Something like the following should do: import builtins import inspect class BaseESpace: def __getattr__(self, name): obj = getattr(builtins, name, None) if inspect.isclass(obj) and issubclass(obj, Exception): return obj else: raise AttributeError(name) class ESpace(BaseESpace): def __init__(self): self.cache = {} def __getattr__(self, name): try: return self.cache[name] except KeyError: custom = type(name, (super().__getattr__(name),), {}) self.cache[name] = custom return custom def func_works(espace=BaseESpace()): raise espace.ValueError('foo') def func_bug(espace=BaseESpace()): int('xyz') espace = ESpace() try: func_works(espace=espace) except espace.ValueError: print('custom exception raised') except ValueError: print('builtin exception raised') try: func_works() except espace.ValueError: print('custom exception raised') except ValueError: print('builtin exception raised') try: func_bug() except espace.ValueError: print('custom exception raised') except ValueError: print('builtin exception raised') So it seems there are plenty of options to realize this in custom projects without a change to the syntax. In any way, these approaches require the API developers to add the extra `espace` parameter to their functions, so all of this can only work based on mutual agreement. Regarding operators, you can always `try / except` and then re-raise a similar exception from `espace`.
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/44WBWP... Code of Conduct: http://python.org/psf/codeofconduct/
Trim! for the sake of all that is holy, and most of what isn't. Soni L. writes:
for starters: operator variants wherever exceptions are used as part of API contract, and add a kwarg to functions that explicitly define exceptions as part of their API contract.
How do I decide when to (1) use None == builtin, I guess? vs. (2) the exception space defined by the new argument vs. (3) a exception space defined by my code vs. (4) a third party's exception space? At this point, the rule is "use whichever exception space helps Soni debug my code", which I don't consider an implementable specification.
if missing, defaults to None. all exceptions not raised in a specific channel will not match a specific channel, but will match None.
so e.g. you do:
So, you've completely missed my point, which is that no, *I* *don't*. You need *other* people to add this keyword argument to *their* functions. I don't see a favorable benefit-cost ratio for me, or for anybody else for that matter. And if other people aren't using it, it's not going to help you with debugging masked bogus exceptions. Steve
It's weird, counterintuitive, unclear syntax to do something already handled much better by simply subclassing exceptions. I reckon that's why push-back. On Fri, Apr 10, 2020, 8:59 AM Soni L. <fakedme+py@gmail.com> wrote:
On 2020-04-10 9:40 a.m., Rhodri James wrote:
On 10/04/2020 12:43, Soni L. wrote:
it's actually fairly common to deal with KeyError instead of using dict.get or w/e.
KeyError is also raised when your code has a bug and your assumptions got broken.
it's fairly easy to get the two different KeyErrors mixed up, at least.
So don't do that, then.
why's a "help us fix bugs related to exception handling" proposal getting so much pushback? I don't understand.
this is a bigger problem in the context of massive frameworks, where you can have layers upon layers of exception handling for all sorts of different stuff. everything from catching KeyError to skip missing entries in a dictionary, in template engines, to... honestly idk what else those massive frameworks like to skip, but I regularly see ppl complaining that their framework's exception handling makes their code undebuggable. and I've been paying attention to python's IRC for years. it's a real problem. I can't be the only person who has noticed this.
sure, these language changes wouldn't magically solve that problem. but currently you can't solve the problem *at all* because there's no way to tell anything apart. the language changes would just be a step in the right direction.
opinions? bikesheds? bring it forward. don't just keep it to yourself. _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/6SQTW5... Code of Conduct: http://python.org/psf/codeofconduct/
On 10/04/20 11:43 pm, Soni L. wrote:
it's actually fairly common to deal with KeyError instead of using dict.get or w/e.
When doing that, though, it's good practice to make sure the try/except encloses the minimum amount of code needed. E.g. instead of try: value = somedict[get_my_key()] except KeyError: value = something_else() it's better to write k = get_my_key() try: value = somedict[k] except KeyError: value = something_else() Then there's no chance of accidentally swallowing a KeyError produced by get_my_key(). -- Greg
On 2020-04-10 11:39 a.m., Greg Ewing wrote:
On 10/04/20 11:43 pm, Soni L. wrote:
it's actually fairly common to deal with KeyError instead of using dict.get or w/e.
When doing that, though, it's good practice to make sure the try/except encloses the minimum amount of code needed.
E.g. instead of
try: value = somedict[get_my_key()] except KeyError: value = something_else()
it's better to write
k = get_my_key() try: value = somedict[k] except KeyError: value = something_else()
Then there's no chance of accidentally swallowing a KeyError produced by get_my_key().
and your custom caching dict raises a KeyError that gets swallowed because of a bug in the cache mechanism. you have solved nothing. the problem still persists. and we still can't get ppl to convert their KeyErrors into RuntimeErrors, or have alternative "error channels" / "exception spaces" other than None. I'll do my part, at least, but unless we make it a pattern (i.e. wrap things in more places, like ValueError in unpacking) it seems like it's gonna be very hard to get ppl to do the same.
On Fri, Apr 10, 2020 at 8:05 PM Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
On 10/04/20 8:30 am, Soni L. wrote:
Sometimes, you have an API:
@abc.abstractmethod def get_property_value(self, prop): """Returns the value associated with the given property.
Args: prop (DataProperty): The property.
Returns: The value associated with the given property.
Raises: PropertyError: If the property is not supported by this config source. LookupError: If the property is supported, but isn't available. ValueError: If the property doesn't have exactly one value. """ raise PropertyError
This doesn't really look like a Python API. It's fairly rare in Python for exceptions to be used to indicate anything other than "something unexpected went wrong".
StopIteration wants to say hello :)
In your unpacking example, the ValueError isn't really intended as something to be caught under normal circumstances. The assumption is that you'll know how many items to expect when you unpack something, and if you don't get that many, then you have a bug.
You could advocate for unpacking to use a more specific exception to facilitate catching it, but I think most people will consider your use case to be quite rare.
Or alternatively, it may be worth separating the function call from the unpacking. But that's not common enough to really need to deal with. ChrisA
On 2020-04-10 10:15 a.m., Chris Angelico wrote:
On Fri, Apr 10, 2020 at 8:05 PM Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
On 10/04/20 8:30 am, Soni L. wrote:
Sometimes, you have an API:
@abc.abstractmethod def get_property_value(self, prop): """Returns the value associated with the given property.
Args: prop (DataProperty): The property.
Returns: The value associated with the given property.
Raises: PropertyError: If the property is not supported by this config source. LookupError: If the property is supported, but isn't available. ValueError: If the property doesn't have exactly one value. """ raise PropertyError
This doesn't really look like a Python API. It's fairly rare in Python for exceptions to be used to indicate anything other than "something unexpected went wrong".
StopIteration wants to say hello :)
and look at what we ended up doing to that one! it now becomes RuntimeError in many places, because it was getting regularly swallowed previously. if that's not an argument for this proposal, idk what is.
In your unpacking example, the ValueError isn't really intended as something to be caught under normal circumstances. The assumption is that you'll know how many items to expect when you unpack something, and if you don't get that many, then you have a bug.
You could advocate for unpacking to use a more specific exception to facilitate catching it, but I think most people will consider your use case to be quite rare.
Or alternatively, it may be worth separating the function call from the unpacking. But that's not common enough to really need to deal with.
fwiw if I raise ValueError it does indicate a bug in *your* code, and shouldn't be caught. so that was actually a bad example! sorry. (altho I can imagine ppl trying to catch it, just because the API defines it... *sigh*)
ChrisA _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/DJLKUO... Code of Conduct: http://python.org/psf/codeofconduct/
On Fri, Apr 10, 2020 at 11:35 PM Soni L. <fakedme+py@gmail.com> wrote:
On 2020-04-10 10:15 a.m., Chris Angelico wrote:
On Fri, Apr 10, 2020 at 8:05 PM Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
On 10/04/20 8:30 am, Soni L. wrote:
Sometimes, you have an API:
@abc.abstractmethod def get_property_value(self, prop): """Returns the value associated with the given property.
Args: prop (DataProperty): The property.
Returns: The value associated with the given property.
Raises: PropertyError: If the property is not supported by this config source. LookupError: If the property is supported, but isn't available. ValueError: If the property doesn't have exactly one value. """ raise PropertyError
This doesn't really look like a Python API. It's fairly rare in Python for exceptions to be used to indicate anything other than "something unexpected went wrong".
StopIteration wants to say hello :)
and look at what we ended up doing to that one! it now becomes RuntimeError in many places, because it was getting regularly swallowed previously.
if that's not an argument for this proposal, idk what is.
It only becomes a RuntimeError in one place that I'm aware of: escaping from a generator function. And that is ONLY because generators are built on top of iterators but have their own way of signalling a non-return. Everywhere that exceptions are used as a non-exceptional form of control flow, it is because the function could return literally any value, and therefore can't signal a non-return in any other way. For instance, __getattr__ has to raise AttributeError if the attribute doesn't exist, because there is no value that could be returned to indicate "attribute doesn't exist". How do you propose to resolve that, with this proposal? Massive massive code overhead so you have to request every exception that might occur? Because, quite frankly, your proposed code is *ugly*. In my totally not-opinionated opinion. :) ChrisA
On 11/04/20 1:15 am, Chris Angelico wrote:
StopIteration wants to say hello :)
By "rare' I mean as a proportion of APIs in existence. StopIteration is frequently used, but it's just one API. The only other one I can think of in the stdlib is EOFError, and that isn't even used all that much. -- Greg
On Sat, Apr 11, 2020 at 12:28 AM Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
On 11/04/20 1:15 am, Chris Angelico wrote:
StopIteration wants to say hello :)
By "rare' I mean as a proportion of APIs in existence. StopIteration is frequently used, but it's just one API.
The only other one I can think of in the stdlib is EOFError, and that isn't even used all that much.
AttributeError, KeyError/IndexError, and GeneratorExit (and StopAsyncIteration) want to say hi too. They're all used as a signal for "there isn't anything I could return here so I have to not return". Even if that isn't an error state, it still has to be signalled with an exception. Unless there's to be a *third* way for a function to indicate something, namely a non-value-return, but then you have to figure out how you receive that, and how you would refactor a function that is capable of returning an object or returning a lack of object. And then this third mechanism would end up looking either like exception handling, or like the return of a "package" object, eg return (True, X) for the successful return and return (False, None) for the unsuccessful. ChrisA
On 11/04/20 2:34 am, Chris Angelico wrote:
AttributeError, KeyError/IndexError, and GeneratorExit (and StopAsyncIteration) want to say hi too.
Okay, there are a few others. But the important thing for our purposes is that APIs designed specifically to use exceptions for flow control (such as StopIteration and GeneratorExit) define their own special exceptions for the purpose. Nothing else raises them, so it's usually fairly safe to catch them. On the other hand, I don't really think of AttributeError, KeyError or IndexError as exceptions intended primarily for flow control. To my mind they're in the same category as TypeError and ValueError -- they signal that you tried to do something that can't be done. While you *can* use them for flow control, you need to be careful how you go about it. And there is usually another way to get the same result that doesn't require catching an exception, e.g, dict.get(). The only one of these that can be a bit of a problem is AttributeError, because there is no other way to attempt to get an attribute that may or may not be there (even hasattr() calls getattr() under the covers and catches AttributeError). Anyway, I'm not denying that there are sometimes problems in this area. But I'm not convinced they're so rampant and widespread that we need a whole new language mechanism to deal with them. -- Greg
On Sat, Apr 11, 2020 at 3:31 AM Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
On 11/04/20 2:34 am, Chris Angelico wrote:
AttributeError, KeyError/IndexError, and GeneratorExit (and StopAsyncIteration) want to say hi too.
Okay, there are a few others. But the important thing for our purposes is that APIs designed specifically to use exceptions for flow control (such as StopIteration and GeneratorExit) define their own special exceptions for the purpose. Nothing else raises them, so it's usually fairly safe to catch them.
On the other hand, I don't really think of AttributeError, KeyError or IndexError as exceptions intended primarily for flow control. To my mind they're in the same category as TypeError and ValueError -- they signal that you tried to do something that can't be done.
While you *can* use them for flow control, you need to be careful how you go about it. And there is usually another way to get the same result that doesn't require catching an exception, e.g, dict.get().
The only one of these that can be a bit of a problem is AttributeError, because there is no other way to attempt to get an attribute that may or may not be there (even hasattr() calls getattr() under the covers and catches AttributeError).
They all serve the same broad purpose - as mentioned, the protocol consists of "return this value" and needs a way to say "there is no value". StopIteration means "there is nothing to yield". AttributeError means "there is no such attribute". KeyError means "there is no such key". None of them are inherently flow control (apart from StopIteration's association with for loops); they're fundamentally about a protocol that has to be capable of returning *any* value, and also capable of signalling the absence of a value. (IMO dict.get() is the same as hasattr() - unless you duplicate the lookup code into it, the most logical way to implement it is to attempt a __getitem__ and, if it raises, return the default.) With every one of these protocols, it's entirely possible to mask a bug *in the protocol function itself* this way. If you're implementing __getitem__, you need to be careful not to accidentally leak a KeyError. One option would be to wrap everything: def __getitem__(self, item): try: if ...: return ... except KeyError: raise RuntimeError # If we didn't hit a return, then the key wasn't found raise KeyError Tada! No leakage. But much more commonly, __getitem__ is going to delegate in a way that means that a "leaked" KeyError is actually the correct behaviour. So it might be necessary to guard just *part* of the function: class LazyMapper: def __init__(self, func, base): ... def __getitem__(self, item): orig = self.base[item] # If this raises, let it raise try: return self.func(orig) # If this raises, it's a bug except KeyError: raise RuntimeError There's no way to solve this problem from the outside. The protocol exists for a reason, and exception handling is an extremely convenient way to delegate the entire protocol. The ONLY way to know which KeyErrors are bugs and which are correct use of protocol is to write the try/except accordingly. With this proposed "espace" mechanism, what you'd end up with is exactly the same, just inverted: def __getitem__(self, item): try: orig = self.base[item] except KeyError: raise KeyError in espace return self.func(orig) You STILL need to indicate which exceptions are proper use of protocol and which are leaks; only now, you can do that with a brand new mechanism with lots of overhead, instead of using the mechanism that we already have. The only reason that StopIteration is special in generators is that they *already* have a way to produce any value or to signal that there are no more values: "yield" and "return". That means it's actually possible to place the guard around the outside; and it also means that the implementation of the function doesn't clearly and obviously make use of an exception as protocol. If you're writing a __next__ method, it should be obvious that StopIteration is significant. If you're calling next(it) inside a __next__ method, then you're clearly and logically delegating. That's normal use of protocol. (And, in fact, there was some pushback to PEP 479 on the basis that generators were legitimately delegating to an unguarded next() call as part of protocol.) If you really want to handle things externally in some way, the only way would be to change the internal and external protocols. Maybe your __getitem__ function could be written to wrap its return value in a tuple, or to return an empty tuple if the item couldn't be found. Then you could have a simple wrapper: def __getitem__(self, key): try: result = self.__real__getitem__(key) except KeyError: raise RuntimeError if result: return result[0] raise KeyError But then it becomes that much harder for __real__getitem__ to delegate to something else. I don't think it really benefits us any. ChrisA
participants (11)
-
Andrew Barnert
-
André Roberge
-
Chris Angelico
-
David Mertz
-
Dominik Vilsmeier
-
Eric V. Smith
-
Greg Ewing
-
Paul Sokolovsky
-
Rhodri James
-
Soni L.
-
Stephen J. Turnbull