Exceptions as function calls to kill boilerplate
I generally find exception objects are really just boilerplate heavy objects masking what I really want them to be, function calls: class MySpecialException(Exception): def __init__(self, some, variables, i, am, tracking): self.some = some ... ... try: if some_test_that_fails(variables): raise MySpecialException(a, b, c, d, e, f) except MySpecialException as e: logger.warning(f"BAD THING {e.a} HAPPENED!") if not handle_it(e.a, e.b, e.c, e.f): raise ... Instead of needing a whole new class definition, wouldn't it be nice to just have something like: .... #notice there isn't a boilerplate custom class created! try: if some_test_that_fails(variables): #I still have a base exception to fall back on for handlers that don't know my special exception raise Exception.my_special_exception(a, b, c, d, e, f) except Exception.my_special_excpetion(a:int, b:str, d, e, f): logger.warning(f"BAD THING {a} HAPPENED!") if not handle_it(a, b, c, f): raise The core idea here is that we are just passing control, so why make exceptions an exception to how control is normally passed, via functions. Just a thought.
Name a language that supports capturing/raising exceptions that allow this. Not only that, Python doesn't even support output variables like C#/.NET does.
I think this can stand on its own without needing to be borrowed from another language. This pattern of needing special objects for exceptions causes me to avoid them since the boilerplate overhead discourages their use.
On 13/10/20 10:16 am, jmward01@gmail.com wrote:
class MySpecialException(Exception): def __init__(self, some, variables, i, am, tracking): self.some = some ... ... try: if some_test_that_fails(variables): raise MySpecialException(a, b, c, d, e, f) except MySpecialException as e: logger.warning(f"BAD THING {e.a} HAPPENED!") if not handle_it(e.a, e.b, e.c, e.f): raise ...
You can reduce the amount of boilerplate by doing something like this: class MySpecialException(Exception): pass def handle_my_special_exception(a, b, c, d, e, f): .... try: if some_test_that_fails(variables): raise MySpecialException(a, b, c, d, e, f) except MySpecialException as e: if not handle_my_special_exception(*e.args): raise -- Greg
Greg Ewing wrote:
You can reduce the amount of boilerplate by doing something like this: class MySpecialException(Exception): pass def handle_my_special_exception(a, b, c, d, e, f): .... try: if some_test_that_fails(variables): raise MySpecialException(a, b, c, d, e, f) except MySpecialException as e: if not handle_my_special_exception(*e.args): raise
Yeah, I could remove some of the argument assignment, but I think your example points out even more strongly how useless that class was in managing the exception. It is literally just a name that I am forced to define outside the flow of logic. If exceptions had less boilerplate they might get used more often with more description. It would be great to get a KeyError like this: try: if c[name] == 'fried chicken': eat(c[name]) except KeyError.missingKey(key): c[key] = getMoreChicken() A little contrived since the key name is available in the same scope, but the clarity of passing a variable along with the exception in a function like syntax makes the handler more obvious and the ease of throwing these named returns would likely lead to more exception handling instead of checking first then doing it. Even without the parameters, this on the fly exception definition would be useful and encourage code clarity. You could do things like: try: r = requests.get(some_url) except HTTPError.notFound(url): print(f"{url} not found. Try another one") except HTTPError: print(f"Unknown HTTP Error that doesn't obviously advertise its self! PANIC") raise Of course, again, looking at the requests code shows just how much boilerplate these things take up. The base class RequestException has some code in it but then there are 22 derived exceptions. So many classes defined just to give a name to an exception and none of them have special parameters guaranteed in the exception that might help you better handle what happened.
Once against, need I remind you that Python doesn't support defining output variables like C#/.NET does, the only way to do it is via the globals/locals dict objects which must either be manually provided or fetched from the stacktrace.
On 10/12/20 10:18 PM, William Pickard wrote:
Once against, need I remind you that Python doesn't support defining output variables like C#/.NET does, the only way to do it is via the globals/locals dict objects which must either be manually provided or fetched from the stacktrace.
Python supports output variables in a limited fashion, you can pass in a mutable object (like a list), that the function can mutate to return an 'output' value. What Python can't do is rebind a name provide by the caller to a new object, thus a parameter bound to an immutable object (like a number or string) can't have its value changed. -- Richard Damon
On Mon, Oct 12, 2020 at 6:52 PM <jmward01@gmail.com> wrote:
Greg Ewing wrote:
You can reduce the amount of boilerplate by doing something like this: class MySpecialException(Exception): pass def handle_my_special_exception(a, b, c, d, e, f): .... try: if some_test_that_fails(variables): raise MySpecialException(a, b, c, d, e, f) except MySpecialException as e: if not handle_my_special_exception(*e.args): raise
Yeah, I could remove some of the argument assignment, but I think your example points out even more strongly how useless that class was in managing the exception. It is literally just a name that I am forced to define outside the flow of logic.
well, yes, if that's all you do. But using a class allows you to take advantage of the class hierarchy: class SpecialValueError(ValueError): pass So that will now be caught by either: except SpecialValueError or exceptValueError or exceptException But if you can't define that name "Outside the flow of logic" -- where the heck ARE you going to define it? As a rule, Exceptions are caught somewhere different than they are raised -- so how does the catching code know what name to catch?!? The other advantage of custom Exceptions is that you can put whatever extra logic or attributes, or whatever you want in them. If you don't need to do any of that, and you don't want to define a name "outside the logic", then why not just use an built in exception? You can still tack on any extra data you want when you raise it: In [3]: try: ...: raise ValueError("a message", "other", "data") ...: except ValueError as err: ...: print("caught my nifty Exception, with extra data:") ...: print(err.args) ...: caught my nifty Exception, with extra data: ('a message', 'other', 'data') If exceptions had less boilerplate they might get used more often with more
description. It would be great to get a KeyError like this:
try: if c[name] == 'fried chicken': eat(c[name]) except KeyError.missingKey(key): c[key] = getMoreChicken()
A little contrived since the key name is available in the same scope, but the clarity of passing a variable along with the exception in a function like syntax makes the handler more obvious and the ease of throwing these named returns would likely lead to more exception handling instead of checking first then doing it.
Can you explain what this is doing? what namespace i"key" in this case?? where did it come from? And a KeyError already has they missing key stored in it: In [10]: try: ...: d['fred'] ...: except KeyError as err: ...: print(f"The key: {err.args[0]} is not in the dict") ...: The key: fred is not in the dict So what are we getting here?
Even without the parameters, this on the fly exception definition would be useful and encourage code clarity.
You could do things like:
try: r = requests.get(some_url) except HTTPError.notFound(url): print(f"{url} not found. Try another one") except HTTPError: print(f"Unknown HTTP Error that doesn't obviously advertise its self! PANIC") raise
I'm confused, can't you already do that kind of thing with requests? Of course, again, looking at the requests code shows just how much
boilerplate these things take up. The base class RequestException has some code in it but then there are 22 derived exceptions. So many classes defined just to give a name to an exception and none of them have special parameters guaranteed in the exception that might help you better handle what happened.
Personally, that's why I tend to use the buillt in exceptions most of the time. But anyway, of those names were not defined in the requests package, where WOULD they be defined? How could you (or Python) possibly know what the heck: HTTPError.notFound was??? If the idea is to get the url that wasn't found, it would have to be in the Exception object somewhere, and there would have to be code tha put it there. Which is what we can already do with Exceptions -- still confused here. -CHB
_______________________________________________ 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/KNGACZ... Code of Conduct: http://python.org/psf/codeofconduct/
-- Christopher Barker, PhD Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython
On 13.10.2020 03:51, jmward01@gmail.com wrote:
Of course, again, looking at the requests code shows just how much boilerplate these things take up. The base class RequestException has some code in it but then there are 22 derived exceptions. So many classes defined just to give a name to an exception and none of them have special parameters guaranteed in the exception that might help you better handle what happened.
Exception classes are often built using inheritance to allow for grouping certain error conditions, e.g. you can catch the RequestException if you want to catch most exceptions raised by the requests package. It's common for more complex packages to come with such a (class) hierarchy and allows for applications using those packages to narrow down their error handling without requiring a deep understanding of the internals of the package. Regarding passing in more context to an exception object: How you design this is really up to you. Because exceptions are regular Python classes you can add methods to them, interpret their arguments in special ways and even put exception resolving code directly into the class. -- Marc-Andre Lemburg eGenix.com Professional Python Services directly from the Experts (#1, Oct 13 2020)
Python Projects, Coaching and Support ... https://www.egenix.com/ Python Product Development ... https://consulting.egenix.com/
::: We implement business ideas - efficiently in both time and costs ::: eGenix.com Software, Skills and Services GmbH Pastor-Loeh-Str.48 D-40764 Langenfeld, Germany. CEO Dipl.-Math. Marc-Andre Lemburg Registered at Amtsgericht Duesseldorf: HRB 46611 https://www.egenix.com/company/contact/ https://www.malemburg.com/
Hi jmward01, I'm not really sure what you mean by "boilerplate heavy objects". *Boilerplate* generally applies to the amount of source code you have to write. I don't think that a two line class (perhaps a couple of extra lines if you give it a docstring) justifies the name "boilerplate": class MySpecialException(Exception): pass In my experience, that covers the majority of custom exceptions I've needed. (More on this below.) As for "heavy objects", it's true that classes are moderately heavyweight: py> sys.getsizeof(Exception) 400 but the instances are relatively lightweight: py> sys.getsizeof(Exception('spam eggs')) 96 and if you're stressed out by that, you might be using the wrong language :-) So I'm not quite sure what specific part of this you are worried about: memory consumption or lines of code. More below. On Mon, Oct 12, 2020 at 09:16:37PM -0000, jmward01@gmail.com wrote:
I generally find exception objects are really just boilerplate heavy objects masking what I really want them to be, function calls:
class MySpecialException(Exception): def __init__(self, some, variables, i, am, tracking): self.some = some ...
To me, that looks like something of a code smell. https://en.wikipedia.org/wiki/Code_smell I am suspicious of exceptions that have lots of values thrown into them. With very few exceptions (pun intended :-) the model of exceptions I expect to see is like the bulk of built-in exceptions. They typically apply to a single value, for a single reason. py> ord(1) --> TypeError: ord() expected string of length 1, but int found So consider that, maybe, your exceptions are doing too much. In your code snippet, the try...except block seems to be literally useless. You are literally raising an exception, only to immediately catch it again, then do a test, and depending on that True/False result of that test, either suppress the exception or re-raise it.
... try: if some_test_that_fails(variables): raise MySpecialException(a, b, c, d, e, f) except MySpecialException as e: logger.warning(f"BAD THING {e.a} HAPPENED!") if not handle_it(e.a, e.b, e.c, e.f): raise ...
I think this is more naturally written without the redundant raise followed by catch: if some_test_that_fails(variables): # presumably "variables" include a, b, c, d, e, f logger.warning(f"BAD THING {a} HAPPENED!") if not handle_it(a, b, c, d, e, f): raise MySpecialException(a, b, c, d, e, f) But even better would be for `handle_it` to be more discriminating. Rather than have it return a flag: True <-- success False <-- "an error occurred" which you handle by raising a catch-all exception that lists no fewer than six things that may have caused the error (a through f), I think that a better design would be for handle_it to directly raise a more specific exception: if some_test_that_fails(variables): # presumably "variables" include a, b, c, d, e, f logger.warning(f"BAD THING {a} HAPPENED!") handle_it(a, b, c, d, e, f) Exceptions raised by `handle_it` probably need only refer to one specific value, not a through f, with a specific exception cause, such as TypeError, ValueError, etc.
Instead of needing a whole new class definition, wouldn't it be nice to just have something like:
.... #notice there isn't a boilerplate custom class created! try: if some_test_that_fails(variables): #I still have a base exception to fall back on for handlers that don't know my special exception raise Exception.my_special_exception(a, b, c, d, e, f)
How does Exception have a `my_special_exception` method? What does it return? It has to return either an exception class or an exception instance, but which class? How does it know that the arguments a, b, .... f (which are anonymous expressions) should be bound to names "a", "b", "c" etc? What if I don't want names "a", etc but something more meaningful? If I call: raise Exception.apeiron_error(None, x or y, obj.widget.status()) what happens? How does Exception know about my apeiron_error method, and what does it do with the arguments passed?
except Exception.my_special_excpetion(a:int, b:str, d, e, f):
How does this know to catch only the exception raised by the `my_special_exception` method? My guess is that you are proposing that some form of structural pattern matching, but precisely what form?
The core idea here is that we are just passing control, so why make exceptions an exception to how control is normally passed, via functions. Just a thought.
Or you could just not use exceptions at all, and just call the function you want to handle the error. -- Steve
Steven, Thanks for the responses. In response to your responses....: Steven D'Aprano wrote:
Hi jmward01, I'm not really sure what you mean by "boilerplate heavy objects". Boilerplate generally applies to the amount of source code you have to write. I don't think that a two line class (perhaps a couple of extra lines if you give it a docstring) justifies the name "boilerplate": class MySpecialException(Exception): pass
In my experience, that covers the majority of custom exceptions I've needed. (More on this below.) As for "heavy objects", it's true that classes are moderately heavyweight: py> sys.getsizeof(Exception) 400
but the instances are relatively lightweight: py> sys.getsizeof(Exception('spam eggs')) 96
and if you're stressed out by that, you might be using the wrong language :-) So I'm not quite sure what specific part of this you are worried about: memory consumption or lines of code. I am not particularly concerned with performance here. I am more worried about code clarity, boilerplate that de-incentivizes a feature and hidden values. The current exception mechanism encourages passing, effectively, un-named lists of parameters or building boilerplate to create the equivalent of named tuples (e.args[0] or building an __init__ that defines members). If the only value is the name, why not just define it where you use it instead of creating an intermediary class definition? I use string literals for state names all the time instead of creating objects and importing them all over my code because most of the time the mental overhead of imports and the like isn't worth the ease and clarity of just passing a string. This is the same argument I am making for most exceptions by suggesting this syntax.
I generally find exception objects are really just boilerplate heavy objects masking what I really want them to be, function calls: class MySpecialException(Exception): def __init__(self, some, variables, i, am, tracking): self.some = some ... To me, that looks like something of a code smell. https://en.wikipedia.org/wiki/Code_smell I am suspicious of exceptions that have lots of values thrown into them. With very few exceptions (pun intended :-) the model of exceptions I expect to see is like the bulk of built-in exceptions. They typically apply to a single value, for a single reason.
More below. On Mon, Oct 12, 2020 at 09:16:37PM -0000, jmward01@gmail.com wrote: py> ord(1) --> TypeError: ord() expected string of length 1, but int found
So consider that, maybe, your exceptions are doing too much. In your code snippet, the try...except block seems to be literally useless. You are literally raising an exception, only to immediately catch it again, then do a test, and depending on that True/False result of that test, either suppress the exception or re-raise it.
Yeah, not the best example I agree. Very contrived. I'll think of a better one that shows more clarity. But not right now. I haven't had my coffee yet. As for variables, the correct amount is the correct amount. I mentally do think of this as just a convoluted function call passing control. When you make that mentality switch then the current mechanism starts looking either obfuscated or bloated for the reasons I said above (passing anonymous lists of parameters, e.args, or adding extra code and defining an object just to pass named parameters). I guess what I am _really_ suggesting here is more of a named return that passes to the first block capable of handling it. That's really what exceptions are conceptually doing, just make it more explicit.
... try: if some_test_that_fails(variables): raise MySpecialException(a, b, c, d, e, f) except MySpecialException as e: logger.warning(f"BAD THING {e.a} HAPPENED!") if not handle_it(e.a, e.b, e.c, e.f): raise ... I think this is more naturally written without the redundant raise followed by catch: if some_test_that_fails(variables): # presumably "variables" include a, b, c, d, e, f logger.warning(f"BAD THING {a} HAPPENED!") if not handle_it(a, b, c, d, e, f): raise MySpecialException(a, b, c, d, e, f)
But even better would be for handle_it to be more discriminating. Rather than have it return a flag: True <-- success False <-- "an error occurred"
which you handle by raising a catch-all exception that lists no fewer than six things that may have caused the error (a through f), I think that a better design would be for handle_it to directly raise a more specific exception: if some_test_that_fails(variables): # presumably "variables" include a, b, c, d, e, f logger.warning(f"BAD THING {a} HAPPENED!") handle_it(a, b, c, d, e, f)
Exceptions raised by handle_it probably need only refer to one specific value, not a through f, with a specific exception cause, such as TypeError, ValueError, etc.
Instead of needing a whole new class definition, wouldn't it be nice to just have something like: .... # notice there isn't a boilerplate custom class created! try: if some_test_that_fails(variables): #I still have a base exception to fall back on for handlers that don't know my special exception raise Exception.my_special_exception(a, b, c, d, e, f)
How does Exception have a my_special_exception method? What does it
return? It has to return either an exception class or an exception instance, but which class? How does it know that the arguments a, b, .... f (which are anonymous expressions) should be bound to names "a", "b", "c" etc? What if I don't want names "a", etc but something more meaningful? If I call: raise Exception.apeiron_error(None, x or y, obj.widget.status())
what happens? How does Exception know about my apeiron_error method, and what does it do with the arguments passed?
except Exception.my_special_excpetion(a:int, b:str, d, e, f): How does this know to catch only the exception raised by the my_special_exception method? My guess is that you are proposing that some form of structural pattern matching, but precisely what form?
I can think of a few ways, but one way is to just think of this as a single named parameter 'subType' to Exception's constructor with some creative code to turn the calls into fancy wrappers around the constructor. Then the exception block is just looking at the exception and the sub type to determine the first handler capable of handling it. Normal function calling conventions apply after that with accompanying exceptions if the function signature doesn't match. You can even define these functions on an exception to make them explicit if you want to and to. The recurring theme to me here though is that functions require definition in two places and are simple: Their def and where they are called. Both sides have to understand the calling arguments and it works well. Exceptions however require definition in three places, the class, the call and the handler and the mechanism to pass arguments is a bit convoluted (either un-named lists or manual parameter assignment). Why not open up the simpler function like mechanism to exception handling? Great discussion. I appreciate the input/responses all!
On 13 Oct 2020, at 16:08, jmward01@gmail.com wrote:
Steven, Thanks for the responses. In response to your responses....: Steven D'Aprano wrote:
Hi jmward01, I'm not really sure what you mean by "boilerplate heavy objects". Boilerplate generally applies to the amount of source code you have to write. I don't think that a two line class (perhaps a couple of extra lines if you give it a docstring) justifies the name "boilerplate": class MySpecialException(Exception): pass
In my experience, that covers the majority of custom exceptions I've needed. (More on this below.) As for "heavy objects", it's true that classes are moderately heavyweight: py> sys.getsizeof(Exception) 400
but the instances are relatively lightweight: py> sys.getsizeof(Exception('spam eggs')) 96
and if you're stressed out by that, you might be using the wrong language :-) So I'm not quite sure what specific part of this you are worried about: memory consumption or lines of code. I am not particularly concerned with performance here. I am more worried about code clarity, boilerplate that de-incentivizes a feature and hidden values. The current exception mechanism encourages passing, effectively, un-named lists of parameters or building boilerplate to create the equivalent of named tuples (e.args[0] or building an __init__ that defines members). If the only value is the name, why not just define it where you use it instead of creating an intermediary class definition? I use string literals for state names all the time instead of creating objects and importing them all over my code because most of the time the mental overhead of imports and the like isn't worth the ease and clarity of just passing a string. This is the same argument I am making for most exceptions by suggesting this syntax.
Using exception.args or an explicit __init__ isn’t necessary: #— (python 3.9) import dataclasses @dataclasses.dataclass class MyException (Exception): a : int b : str try: raise MyException(a=1, b="hello") except MyException as exc: print(f"{exc.a=}, {exc.b=}”) # — Ronald — Twitter / micro.blog: @ronaldoussoren Blog: https://blog.ronaldoussoren.net/
On Wed, Oct 14, 2020 at 1:41 AM Ronald Oussoren via Python-ideas <python-ideas@python.org> wrote:
#— (python 3.9)
import dataclasses
@dataclasses.dataclass class MyException (Exception): a : int b : str
try: raise MyException(a=1, b="hello")
except MyException as exc: print(f"{exc.a=}, {exc.b=}”)
# —
I like it. Very elegant. Never thought of a dataclass as an exception. ChrisA
On 10/13/2020 11:09 AM, Chris Angelico wrote:
On Wed, Oct 14, 2020 at 1:41 AM Ronald Oussoren via Python-ideas <python-ideas@python.org> wrote:
#— (python 3.9)
import dataclasses
@dataclasses.dataclass class MyException (Exception): a : int b : str
try: raise MyException(a=1, b="hello")
except MyException as exc: print(f"{exc.a=}, {exc.b=}”)
# —
I like it. Very elegant. Never thought of a dataclass as an exception.
That's the beauty of dataclasses not using metaclasses or inheritance: they can be used pretty much anywhere, with very few adverse interactions. All credit to attrs for this design. Eric
On Tue, Oct 13, 2020 at 02:08:11PM -0000, jmward01@gmail.com wrote:
I am not particularly concerned with performance here. I am more worried about code clarity, boilerplate that de-incentivizes a feature and hidden values.
Okay.
The current exception mechanism encourages passing, effectively, un-named lists of parameters or building boilerplate to create the equivalent of named tuples (e.args[0] or building an __init__ that defines members).
I don't agree that it *encourages* that, more that it allows it. I'd like to see the typical use you make of exceptions. For me, almost without exception (pun intended) the exception is intended for three purposes: 1. A human-readable error message: # expecting a prime number, but didn't receive one raise ValueError('argument is not a prime number') 2. A low-resolution mechanism for re-trying or suppressing errors: for number in numbers: try: process(number) # May raise ValueError except ValueError: print("skipping non-prime") 3. A simple protocol for returning an out-of-bounds state flag, in the way StopIteration is used. (Number 3 is technically a form of flow-control, but in practice the way it is used is quite simple.) For those three purposes, I think exceptions are fine. But you seem to be using exceptions as some sort of dispatch mechanism.
If the only value is the name, why not just define it where you use it instead of creating an intermediary class definition?
I use string literals for state names all the time instead of creating objects and importing them all over my code because most of the time the mental overhead of imports and the like isn't worth the ease and clarity of just passing a string.
I don't see that this situation is relevant. Your exception handling code seems to make use of a lot of internal state: you store variables as exception attributes, then extract them later. While I guess you could do that with strings: string = f'{spam}; {eggs}; {cheese}; {aardvark}' # later on... spam, eggs, cheese, aardvar = string.split(';') I'm guessing you don't. But that's what you're doing with the exceptions: you store a bunch of values into the exception object, then raise the exception, only to *immediately* catch it again, retrieve those same values, and use them. Why not just use the values directly? (Based on your earlier pseudo-code example. Perhaps real code is different.) [...]
I mentally do think of this as just a convoluted function call passing control.
I try to avoid convoluted code of any description. Sometimes I fail, but I don't do it intentionally.
When you make that mentality switch then the current mechanism starts looking either obfuscated or bloated for the reasons I said above (passing anonymous lists of parameters, e.args, or adding extra code and defining an object just to pass named parameters).
Doesn't that suggest to you that maybe you are using the wrong tool for the job, or using it badly? If serving soup with a carving knife requires you to create an obfuscated, bloated, convoluted framework, perhaps you would be better off using a ladle like everyone else :-) I do look forward to seeing a more realistic example of what you are doing, because from the pseudo-code you supplied earlier, it looks like an obfuscated, bloated, convoluted way of doing something that is simple if you just don't use exceptions.
I guess what I am _really_ suggesting here is more of a named return that passes to the first block capable of handling it. That's really what exceptions are conceptually doing, just make it more explicit.
Hmmm, are you thinking of message-passing mechanisms? Your description sounds like Hypertalk and other XTalk programming languages, such as Apple's Hypercard in the 1990s. To call a function, you would send a message to another widget. If that widget could handle it, it would execute the appropriate message handler. If not, the message would fall through to the next level of the message-handling hierarchy: widget card background window application interpreter In Hypercard this was based on a GUI framework, but the principle doesn't just apply to GUIs. I think that the Actor model of computation is similar. -- Steve
https://docs.python.org/3/faq/design.html#how-fast-are-exceptions : """ A try/except block is extremely efficient if no exceptions are raised. Actually catching an exception is expensive. In versions of Python prior to 2.0 it was common to use this idiom: try: value = mydict[key] except KeyError: mydict[key] = getvalue(key) value = mydict[key] This only made sense when you expected the dict to have the key almost all the time. If that wasn’t the case, you coded it like this: if key in mydict: value = mydict[key] else: value = mydict[key] = getvalue(key) For this specific case, you could also use value = dict.setdefault(key, getvalue(key)), but only if the getvalue() call is cheap enough because it is evaluated in all cases. """ And then the next FAQ is "Why isn’t there a switch or case statement in Python?" https://docs.python.org/3/faq/design.html#why-isn-t-there-a-switch-or-case-s... - [ ] DOC: Which should probably be updated to mention the new Structural Pattern Matching PEP 622 and 634 "PEP 622 -- Structural Pattern Matching" https://www.python.org/dev/peps/pep-0622/ "PEP 634 -- Structural Pattern Matching: Specification" https://www.python.org/dev/peps/pep-0634/ Are there docs for patten matching (the match and case keywords) yet? Which PEPs explain the rationale for doing away with string exceptions? ... https://en.m.wikipedia.org/wiki/Message_passing lists programming languages that include message passing as a core language feature and also links to the actor model (which is usually applied for concurrent applications) https://en.wikipedia.org/wiki/Message_passing#Message-passing_versus_calling Are Exceptions slower because they carry a traceback reference to a stack frame? Why else are Exceptions slower than conditionals and/or structural pattern matching? ( FWIU, pykka is similar to the Akka actor model framework and predates the new structural pattern matching functionality https://www.pykka.org/en/latest/quickstart/#rules-of-the-actor-model ) On Tue, Oct 13, 2020, 9:25 PM Steven D'Aprano <steve@pearwood.info> wrote:
On Tue, Oct 13, 2020 at 02:08:11PM -0000, jmward01@gmail.com wrote:
I am not particularly concerned with performance here. I am more worried about code clarity, boilerplate that de-incentivizes a feature and hidden values.
Okay.
The current exception mechanism encourages passing, effectively, un-named lists of parameters or building boilerplate to create the equivalent of named tuples (e.args[0] or building an __init__ that defines members).
I don't agree that it *encourages* that, more that it allows it.
I'd like to see the typical use you make of exceptions. For me, almost without exception (pun intended) the exception is intended for three purposes:
1. A human-readable error message:
# expecting a prime number, but didn't receive one raise ValueError('argument is not a prime number')
2. A low-resolution mechanism for re-trying or suppressing errors:
for number in numbers: try: process(number) # May raise ValueError except ValueError: print("skipping non-prime")
3. A simple protocol for returning an out-of-bounds state flag, in the way StopIteration is used.
(Number 3 is technically a form of flow-control, but in practice the way it is used is quite simple.)
For those three purposes, I think exceptions are fine. But you seem to be using exceptions as some sort of dispatch mechanism.
If the only value is the name, why not just define it where you use it instead of creating an intermediary class definition?
I use string literals for state names all the time instead of creating objects and importing them all over my code because most of the time the mental overhead of imports and the like isn't worth the ease and clarity of just passing a string.
I don't see that this situation is relevant. Your exception handling code seems to make use of a lot of internal state: you store variables as exception attributes, then extract them later. While I guess you could do that with strings:
string = f'{spam}; {eggs}; {cheese}; {aardvark}' # later on... spam, eggs, cheese, aardvar = string.split(';')
I'm guessing you don't. But that's what you're doing with the exceptions: you store a bunch of values into the exception object, then raise the exception, only to *immediately* catch it again, retrieve those same values, and use them. Why not just use the values directly?
(Based on your earlier pseudo-code example. Perhaps real code is different.)
[...]
I mentally do think of this as just a convoluted function call passing control.
I try to avoid convoluted code of any description. Sometimes I fail, but I don't do it intentionally.
When you make that mentality switch then the current mechanism starts looking either obfuscated or bloated for the reasons I said above (passing anonymous lists of parameters, e.args, or adding extra code and defining an object just to pass named parameters).
Doesn't that suggest to you that maybe you are using the wrong tool for the job, or using it badly? If serving soup with a carving knife requires you to create an obfuscated, bloated, convoluted framework, perhaps you would be better off using a ladle like everyone else :-)
I do look forward to seeing a more realistic example of what you are doing, because from the pseudo-code you supplied earlier, it looks like an obfuscated, bloated, convoluted way of doing something that is simple if you just don't use exceptions.
I guess what I am _really_ suggesting here is more of a named return that passes to the first block capable of handling it. That's really what exceptions are conceptually doing, just make it more explicit.
Hmmm, are you thinking of message-passing mechanisms?
Your description sounds like Hypertalk and other XTalk programming languages, such as Apple's Hypercard in the 1990s. To call a function, you would send a message to another widget. If that widget could handle it, it would execute the appropriate message handler. If not, the message would fall through to the next level of the message-handling hierarchy:
widget card background window application interpreter
In Hypercard this was based on a GUI framework, but the principle doesn't just apply to GUIs. I think that the Actor model of computation is similar.
-- Steve _______________________________________________ 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/XCQEZ3... Code of Conduct: http://python.org/psf/codeofconduct/
jmward01@gmail.com writes:
I am not particularly concerned with performance here. I am more worried about code clarity, boilerplate that de-incentivizes a feature and hidden values. The current exception mechanism encourages passing, effectively, un-named lists of parameters or building boilerplate to create the equivalent of named tuples (e.args[0] or building an __init__ that defines members).
You use a lot of deprecatory words that don't apply to any exception handling code I've seen as far as I can tell. Examples (preferably from code that has been reviewed and revised and still is ugly) would help a lot.
I guess what I am _really_ suggesting here is more of a named return that passes to the first block capable of handling it.
That might be interesting if you had a definition and offered use cases. For example, as a contrast with "named return", a couple of months back there was a long discussion of "named breaks" (ie, breaking multiple levels of nested loops, which can be implemented with exceptions or flags and conditionals, but neither is very pretty). The concept is well-defined by those two words, I think, but it fell down and didn't get up because the use cases are scarce and the syntax proposals were not copacetic. A well-defined named return concept might have legs because it sounds like it would be more powerful than named break. But I think of the command "return from stack frame" provided by many debuggers when I see "named return". I don't think you're talking about that. "Coming home to a place he'd never been before" is not a return, not even a named one. That is, catching an exception is not a return to the place of "the" call in the control flow, it's a non-local jump to independent code that happens to be lexically adjacent to some try suite containing a parent call that descends to calling the function containing the raise statement. Note the ambiguity of the "the" in scare quotes above. Also unlike return, where normal flow of control resumes after *the* call point, there's no presumption that the handler associated with this try will ever "see" the exception: it could be "intercepted" by another try statement in called code -- the exception mechanism is *designed* for that. In most of the code I work on, in fact, I desperately hope it gets intercepted because if it gets to my function, I have to give up -- the only thing I can do to help is to give up gracefully. In some sense, this isn't fair to you, because I'm imposing my understanding on your words. My intent is to convey the cognitive dissonance I feel reading your posts.
Exceptions however require definition in three places,
In the general case, four mentions. See below.
the class, the call and the handler and the mechanism to pass arguments is a bit convoluted (either un-named lists or manual parameter assignment).
The argument-passing mechanism is the same as in any Python callable. I don't understand what you think is convoluted about it. The only way I can understand it is that you have a conventional pattern of use that you'd like to implement as Python syntax (ie, syntactic sugar), but the only pattern I'm familiar with is raise SomeException('explanatory string', offending_object) and even then, that exception object is accessed only very rarely (except by top-level code that logs and/or prints it, and there the simple structure of e.args usually makes "print(*e.args)" all you need). If I needed access to the context of the raise in the handler, class SomeExceptionWithContext(SomeException): def __init__(self, *args, **kwargs): self.args = args self.context = kwargs def foo(): raise SomeExceptionWithContext('explanatory string', offending_object, a=a, b=b) seems to be a nice-enough, lightweight-enough, quite general pattern that would do what I need. I write "seems" because I've never wanted, let alone used, this construct. It would help to have examples from actual programs of tangled-ball- of-string code that a "function-like" exception creation and catching mechanism would help straighten out.
Why not open up the simpler function like mechanism to exception handling?
Because exceptions are frequently used to communicate across module boundaries, I don't see how to avoid (as a practical minimum in the general case) four mentions: definition and raise in one module, and import and catch in another. We could define abbreviated syntax for common use cases, but what are those cases? Adding syntax is a heavy lift, and the define-or-import-class implementation has significant readability and typo-checking benefits to me at least. Regards,
On Tue, Oct 13, 2020 at 6:18 AM Steven D'Aprano <steve@pearwood.info> wrote:
I don't think that a two line class (perhaps a couple of extra lines if you give it a docstring) justifies the name "boilerplate":
class MySpecialException(Exception): pass
I think that in 22 years of using Python, I have never written an exception that took more than these two lines of code. Heck, I even have my memory jogged of string exceptions reading this. When did those go away fully, 1.5.2? 2.1? I DID in the discussion, immediately think of making an exception a dataclass, as someone else replied with. I guess if you want cargo in your exception, that's a pretty good way to do it. But really the ONLY thing I ever want in an exception is an inheritance tree. An exception feels like a really unnatural way to pass around data (that said, there are a few exceptions written by other libraries where some attribute or another is useful in my except blocks. Perhaps I should consider that, beyond inspecting the traceback when needed. If you really want a snazzy highly-parameterized exception, write it yourself as a class factory. I won't particularly like the antipattern, but it's available now. if some_bad_thing: raise ExceptionMaker("BadStuffErrror", details, plus, more_details) Implementation of 'ExceptionMaker' left to readers. But it's possible to write once.
Exception.args holds the arguments passed to the exception. You could add type checking with dataclass, or you can just access Exception.args or sys.exc_info().args: ```python class Exception2(Exception): pass try: raise Exception2('error str', dict(a='1', b=2)) except Exception2 as e: print(('args', e.args)) assert e.args[0] == 'error str' assert e.args[1] == dict(a='1', b=2) import sys type_, value, traceback = sys.exc_info() assert value.args[0] == 'error str' assert value.args[1] == dict(a='1', b=2) # ('args', ('error str', {'a': '1', 'b': 2})) ``` On Tue, Oct 13, 2020 at 12:56 PM David Mertz <mertz@gnosis.cx> wrote:
On Tue, Oct 13, 2020 at 6:18 AM Steven D'Aprano <steve@pearwood.info> wrote:
I don't think that a two line class (perhaps a couple of extra lines if you give it a docstring) justifies the name "boilerplate":
class MySpecialException(Exception): pass
I think that in 22 years of using Python, I have never written an exception that took more than these two lines of code.
Heck, I even have my memory jogged of string exceptions reading this. When did those go away fully, 1.5.2? 2.1?
I DID in the discussion, immediately think of making an exception a dataclass, as someone else replied with. I guess if you want cargo in your exception, that's a pretty good way to do it. But really the ONLY thing I ever want in an exception is an inheritance tree. An exception feels like a really unnatural way to pass around data (that said, there are a few exceptions written by other libraries where some attribute or another is useful in my except blocks. Perhaps I should consider that, beyond inspecting the traceback when needed.
If you really want a snazzy highly-parameterized exception, write it yourself as a class factory. I won't particularly like the antipattern, but it's available now.
if some_bad_thing: raise ExceptionMaker("BadStuffErrror", details, plus, more_details)
Implementation of 'ExceptionMaker' left to readers. But it's possible to write once.
_______________________________________________ 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/M7LP4O... Code of Conduct: http://python.org/psf/codeofconduct/
Exception handling is often not handled correctly in software, especially when there are multiple sources of exceptions; data flow analysis of 5 million lines of Java code found over 1300 exception handling defects.[15] Citing multiple prior studies by others (1999–2004) and their own results, Weimer and Necula wrote that a significant problem with exceptions is that
Compared to conditionals, programming by Exception is slow. You can use the timeit module (or the IPython %timeit magic function) to run comparative performance benchmarks. https://en.wikipedia.org/wiki/Coding_by_exception : https://en.wikipedia.org/wiki/Exception_handling#Criticism : they "create hidden control-flow paths that are difficult for programmers to reason about".[15]:8:27
Go was initially released with exception handling explicitly omitted,
** advise using only for unrecoverable errors that should halt the entire
with the developers arguing that it obfuscated control flow.[16] Later, the exception-like panic/recover mechanism was added to the language, which the Go authors process.[17][18][19][20] **
Exceptions, as unstructured flow, increase the risk of resource leaks
(such as escaping a section locked by a mutex, or one temporarily holding a file open) or inconsistent state. There are various techniques for resource management in the presence of exceptions, most commonly combining the dispose pattern with some form of unwind protection (like a finally clause), which automatically releases the resource when control exits a section of code. [Emphasis added] Neither of these documents advise against using Exceptions for control flow: - https://docs.python.org/3/tutorial/errors.html#exceptions - https://docs.python.org/3/library/exceptions.html One alternative to Exceptions is to return a (value, err) tuple and have conditionals for each possible value of err. (value, err) = func() if err is not None: # [...] On Tue, Oct 13, 2020 at 1:10 PM Wes Turner <wes.turner@gmail.com> wrote:
Exception.args holds the arguments passed to the exception.
You could add type checking with dataclass, or you can just access Exception.args or sys.exc_info().args:
```python class Exception2(Exception): pass
try: raise Exception2('error str', dict(a='1', b=2)) except Exception2 as e: print(('args', e.args)) assert e.args[0] == 'error str' assert e.args[1] == dict(a='1', b=2)
import sys type_, value, traceback = sys.exc_info() assert value.args[0] == 'error str' assert value.args[1] == dict(a='1', b=2)
# ('args', ('error str', {'a': '1', 'b': 2})) ```
On Tue, Oct 13, 2020 at 12:56 PM David Mertz <mertz@gnosis.cx> wrote:
On Tue, Oct 13, 2020 at 6:18 AM Steven D'Aprano <steve@pearwood.info> wrote:
I don't think that a two line class (perhaps a couple of extra lines if you give it a docstring) justifies the name "boilerplate":
class MySpecialException(Exception): pass
I think that in 22 years of using Python, I have never written an exception that took more than these two lines of code.
Heck, I even have my memory jogged of string exceptions reading this. When did those go away fully, 1.5.2? 2.1?
I DID in the discussion, immediately think of making an exception a dataclass, as someone else replied with. I guess if you want cargo in your exception, that's a pretty good way to do it. But really the ONLY thing I ever want in an exception is an inheritance tree. An exception feels like a really unnatural way to pass around data (that said, there are a few exceptions written by other libraries where some attribute or another is useful in my except blocks. Perhaps I should consider that, beyond inspecting the traceback when needed.
If you really want a snazzy highly-parameterized exception, write it yourself as a class factory. I won't particularly like the antipattern, but it's available now.
if some_bad_thing: raise ExceptionMaker("BadStuffErrror", details, plus, more_details)
Implementation of 'ExceptionMaker' left to readers. But it's possible to write once.
_______________________________________________ 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/M7LP4O... Code of Conduct: http://python.org/psf/codeofconduct/
An issue I have is how to handle typos, whether implemented as a helper or a __getattr__ method: try: ... raise ExceptionMaker.color_error("color out of range", color) except ExceptionMaker.colr_error as e: ... Each __getattr__ would dynamically create/return a new exception class based on the name. and the raised error will get missed either because the raised error was valid but the caught error was wrong or vice versa. At least with the way exceptions are normally handled, a NameError would be raised if the exception name doesn't exist. On 10/13/20 12:47 PM, David Mertz wrote:
On Tue, Oct 13, 2020 at 6:18 AM Steven D'Aprano <steve@pearwood.info <mailto:steve@pearwood.info>> wrote:
I don't think that a two line class (perhaps a couple of extra lines if you give it a docstring) justifies the name "boilerplate":
class MySpecialException(Exception): pass
I think that in 22 years of using Python, I have never written an exception that took more than these two lines of code.
Heck, I even have my memory jogged of string exceptions reading this. When did those go away fully, 1.5.2? 2.1?
I DID in the discussion, immediately think of making an exception a dataclass, as someone else replied with. I guess if you want cargo in your exception, that's a pretty good way to do it. But really the ONLY thing I ever want in an exception is an inheritance tree. An exception feels like a really unnatural way to pass around data (that said, there are a few exceptions written by other libraries where some attribute or another is useful in my except blocks. Perhaps I should consider that, beyond inspecting the traceback when needed.
If you really want a snazzy highly-parameterized exception, write it yourself as a class factory. I won't particularly like the antipattern, but it's available now.
if some_bad_thing: raise ExceptionMaker("BadStuffErrror", details, plus, more_details)
Implementation of 'ExceptionMaker' left to readers. But it's possible to write once.
_______________________________________________ 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/M7LP4O... Code of Conduct: http://python.org/psf/codeofconduct/
On Tue, Oct 13, 2020 at 9:55 AM David Mertz <mertz@gnosis.cx> wrote:
If you really want a snazzy highly-parameterized exception, write it yourself as a class factory. I won't particularly like the antipattern, but it's available now.
if some_bad_thing: raise ExceptionMaker("BadStuffErrror", details, plus, more_details)
Implementation of 'ExceptionMaker' left to readers. But it's possible to write once.
I was thinking the same thing, and you could even write a fancy __getattrr__, so you could do: raise ExceptionMaker.BadStuffErrror(details, plus, more_details) But here's where I am still completely confused. In order to catch that Exception, you need know about it -- it's name, what parameters it uses, etc. And that will be potentially very far from the code that creates it. And what if you want to raise the same (or similar) exception somewhere else? The OP has stated that all making an Exception class does is create a name -- which is true (though the other thing it does is put that name in a class hierarchy) -- but the thing is -- that's very useful! In fact, I can't see how you could do this at all without creating a name somewhere that it could be accessed from elsewhere. -CHB
_______________________________________________ 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/M7LP4O... Code of Conduct: http://python.org/psf/codeofconduct/
-- Christopher Barker, PhD Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython
On Tue, Oct 13, 2020 at 1:21 PM Christopher Barker <pythonchb@gmail.com> wrote:
On Tue, Oct 13, 2020 at 9:55 AM David Mertz <mertz@gnosis.cx> wrote:
If you really want a snazzy highly-parameterized exception, write it yourself as a class factory. I won't particularly like the antipattern, but it's available now.
if some_bad_thing: raise ExceptionMaker("BadStuffError", details, plus, more_details)
Implementation of 'ExceptionMaker' left to readers. But it's possible to write once.
raise ExceptionMaker.BadStuffError(details, plus, more_details)
But here's where I am still completely confused. In order to catch that Exception, you need know about it -- it's name, what parameters it uses, etc. And that will be potentially very far from the code that creates it.
Yeah, I thought of that. I think the class factory would need to monkey-patch the global namespace. However, there's already a really clear way of "monkey-patching the global namespace": class BadStuffError(Exception): pass Sure, you can accomplish the same thing by overloading .__getattr__(). But the usual means is pretty nice. The OP has stated that all making an Exception class does is create a name
-- which is true (though the other thing it does is put that name in a class hierarchy) -- but the thing is -- that's very useful! In fact, I can't see how you could do this at all without creating a name somewhere that it could be accessed from elsewhere.
Well, technically you could memoize calls to ExceptionMaker and have this work. In [11]: try: ...: raise ExceptionMaker("BadStuffError") ...: except ExceptionMaker("BadStuffError"): ...: print("caught") ...: caught I am NOT recommending this pattern. But it works in current Python.
On Wed, Oct 14, 2020 at 4:48 AM David Mertz <mertz@gnosis.cx> wrote:
Well, technically you could memoize calls to ExceptionMaker and have this work.
In [11]: try: ...: raise ExceptionMaker("BadStuffError") ...: except ExceptionMaker("BadStuffError"): ...: print("caught") ...: caught
I am NOT recommending this pattern. But it works in current Python.
ExceptionMaker = defaultdict(lambda name: type(name, (Exception,), {})) And use square brackets. ChrisA
I keep using a syntax of <SomeActualException>.<someSubType> for a reason. It is piggybacking on an already established exception and in reality just providing a sub-type that could easily be used by the except block to determine acceptance. An easy implementation of this still constructs <SomeActualException> that just has a property of subtype so if you handle <SomeActualException> explicitly using the current syntax it would catch it and the parameters you fed in the raise would just be in the e.args.
I've heard of this thing called "multiple inheritance" :-). Does that fit into your naming scheme? On Tue, Oct 13, 2020 at 2:03 PM <jmward01@gmail.com> wrote:
I keep using a syntax of <SomeActualException>.<someSubType> for a reason. It is piggybacking on an already established exception and in reality just providing a sub-type that could easily be used by the except block to determine acceptance. An easy implementation of this still constructs <SomeActualException> that just has a property of subtype so if you handle <SomeActualException> explicitly using the current syntax it would catch it and the parameters you fed in the raise would just be in the e.args. _______________________________________________ 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/HTBOVA... Code of Conduct: http://python.org/psf/codeofconduct/
-- The dead increasingly dominate and strangle both the living and the not-yet born. Vampiric capital and undead corporate persons abuse the lives and control the thoughts of homo faber. Ideas, once born, become abortifacients against new conceptions.
I am not advocating removing exception objects so inheritance is still there if needed. As for the data class and exceptions being just two lines, again, I think that makes my point about the boilerplate for most exceptions. Most of the time they are being created just to have a constant to compare against in your except statement so they have little value other than a name. Why waste the lines defining them anywhere other than where they are thrown and where they are handled for most of them? There _are_ times when you want a real object, along with the inheritance and functionality it provides and I am not advocating removing that, but most of this discussion keeps going back to 'Yeah, I just create empty exception objects for the name if I create them at all'. Lets simplify that use case and make it a little more powerful. That is the boilerplate argument and I think that alone would be worth while even if the argument stuff wasn't added but I think variables and the mentality shift that enables would really be nice. I think that people think of exceptions as a signal with no payload/obfuscated payload because that is how they have seen them, but they don't have to be that way. Exceptions exist because an alternative return is needed, why not allow that communication to be more function like and advertise its parameters like a function would? When I deal with other libraries, like requests, I keep spending time trying to figure out what variables an exception has. There are clearly times where a variable is needed as HTTPError points out. But, where do I get the error code? It isn't obvious from the definition of HTTPError and this obfuscation is a constant issue with exceptions from a lot of libraries. A syntax like what I propose could help clarify the raise except contract and bring obfuscated parameters like code to light. I do agree that it is nice to have some ability to type the call more rigidly, just like a function call. I could see having functions, optionally, advertise exception calls as possible alternate returns but this should be just that, optional just to help out clarity. No need to force an external definition here. This idea is still clearly 'rough' from a syntax/full integration into the language point of view and if it gathers momentum there will be a lot of edge cases to think about, this one included. Finally, I thought about the ExceptionMaker idea myself, to me the handling side is the bigger issue and not the raise side and this doesn't clean that up. The goal is to allow an elegant way of allowing control flow to change in the caller code. It should make it obvious what you are handling along with what variables are related to the flow change. Finally, it should minimize require extra code that, to me, adds little value. This just sounds like a function call, so lets re-use that.
I think that Exceptions are not designed for message passing: they're deliberately kept very simple because (1) exceptions are slow; and (2) they create scope and resource considerations (as I directly quoted above). Why do you need subclasses for this? dataobj = dict(a=a, b=b, c=c) try: func() raise MyException("exception message {a}".format(**dataobj), dataobj) except MyException as e: logger.exception(e) print("additional text {b}".format(**dataobj)) print("additional text {b}".format(**e.args[1])) raise MySpecialException("additionally") from e Is there a reason to not return a result object with an .error field (or a value, err tuple) instead of using Exceptions for message passing? On Tue, Oct 13, 2020 at 1:58 PM <jmward01@gmail.com> wrote:
I am not advocating removing exception objects so inheritance is still there if needed. As for the data class and exceptions being just two lines, again, I think that makes my point about the boilerplate for most exceptions. Most of the time they are being created just to have a constant to compare against in your except statement so they have little value other than a name. Why waste the lines defining them anywhere other than where they are thrown and where they are handled for most of them? There _are_ times when you want a real object, along with the inheritance and functionality it provides and I am not advocating removing that, but most of this discussion keeps going back to 'Yeah, I just create empty exception objects for the name if I create them at all'. Lets simplify that use case and make it a little more powerful.
That is the boilerplate argument and I think that alone would be worth while even if the argument stuff wasn't added but I think variables and the mentality shift that enables would really be nice. I think that people think of exceptions as a signal with no payload/obfuscated payload because that is how they have seen them, but they don't have to be that way. Exceptions exist because an alternative return is needed, why not allow that communication to be more function like and advertise its parameters like a function would?
When I deal with other libraries, like requests, I keep spending time trying to figure out what variables an exception has. There are clearly times where a variable is needed as HTTPError points out. But, where do I get the error code? It isn't obvious from the definition of HTTPError and this obfuscation is a constant issue with exceptions from a lot of libraries. A syntax like what I propose could help clarify the raise except contract and bring obfuscated parameters like code to light.
I do agree that it is nice to have some ability to type the call more rigidly, just like a function call. I could see having functions, optionally, advertise exception calls as possible alternate returns but this should be just that, optional just to help out clarity. No need to force an external definition here. This idea is still clearly 'rough' from a syntax/full integration into the language point of view and if it gathers momentum there will be a lot of edge cases to think about, this one included.
Finally, I thought about the ExceptionMaker idea myself, to me the handling side is the bigger issue and not the raise side and this doesn't clean that up. The goal is to allow an elegant way of allowing control flow to change in the caller code. It should make it obvious what you are handling along with what variables are related to the flow change. Finally, it should minimize require extra code that, to me, adds little value. This just sounds like a function call, so lets re-use that. _______________________________________________ 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/WYGWE2... Code of Conduct: http://python.org/psf/codeofconduct/
On Tue, Oct 13, 2020 at 1:56 PM <jmward01@gmail.com> wrote:
I do agree that it is nice to have some ability to type the call more rigidly, just like a function call. I could see having functions, optionally, advertise exception calls as possible alternate returns but this should be just that, optional just to help out clarity.
OK. But in 22 years, I've never had a desire to raise with custom args, and I've often had a desire to express inheritance relationships. So this need feels to me to be rare, at most (and probably usually an antipattern). That said, the ExceptionMaker that I proposed gets you exactly what you want already. I like Chris' idea of defaultdict and square brackets even better than mine about memoization (maybe with @functools.lru_cache). Either way, you already HAVE what you are asking for, except few people actually want to use it. -- The dead increasingly dominate and strangle both the living and the not-yet born. Vampiric capital and undead corporate persons abuse the lives and control the thoughts of homo faber. Ideas, once born, become abortifacients against new conceptions.
On 14/10/20 6:55 am, jmward01@gmail.com wrote:
Why waste the lines defining them anywhere other than where they are thrown and where they are handled for most of them? If you expect people to catch these exceptions, they need to be documented somewhere. A class statement does a much better job of that than a raise statement buried somewhere in the guts of your code.
When I deal with other libraries, like requests, I keep spending time to figure out what variables an exception has.
The vast majority of the time, when I catch an exception the only thing I want from it is an error message, and str(e) does perfectly well for that. If I need more, then if the library is well-written, help() on the exception class will tell me everything I need to know. What am I supposed to do to find out about one of your proposed classless exceptions? -- Greg
On Tue, Oct 13, 2020 at 12:47:45PM -0400, David Mertz wrote:
On Tue, Oct 13, 2020 at 6:18 AM Steven D'Aprano <steve@pearwood.info> wrote:
I don't think that a two line class (perhaps a couple of extra lines if you give it a docstring) justifies the name "boilerplate":
class MySpecialException(Exception): pass
I think that in 22 years of using Python, I have never written an exception that took more than these two lines of code.
I have, once or twice in a blue moon, written more complex exceptions, but few of them survived long enough to make it into actual working code.
Heck, I even have my memory jogged of string exceptions reading this. When did those go away fully, 1.5.2? 2.1?
String exceptions hung around a lot longer than people think, they were first deprecated in 2.5 and not removed for good until 2.6. $ python2.5 -c "raise 'spam and eggs'" -c:1: DeprecationWarning: raising a string exception is deprecated Traceback (most recent call last): File "<string>", line 1, in <module> spam and eggs
I DID in the discussion, immediately think of making an exception a dataclass, as someone else replied with. I guess if you want cargo in your exception, that's a pretty good way to do it.
"Cargo", that's an excellent term, thank you.
But really the ONLY thing I ever want in an exception is an inheritance tree. An exception feels like a really unnatural way to pass around data
Indeed. Very few exceptions in the std lib are used that way, which hints that it may not be a great idiom, or at least not a great cultural fit. As Wes pointed out, catching exceptions is relatively slow, and using exceptions for flow control makes them a glorified GOTO with all the risks that entails. Or even COMEFROM. Urggh. I don't entirely agree with this article: http://www.lighterra.com/papers/exceptionsharmful/ but the points it makes are important. See also: https://stackoverflow.com/questions/1736146/why-is-exception-handling-bad https://softwareengineering.stackexchange.com/questions/189222/are-exception... So I'm cautious about using exceptions for complex flow control. (Iteration and StopIteration is a very simple example of flow control.) -- Steve
On Tue, Oct 13, 2020 at 7:02 PM Steven D'Aprano <steve@pearwood.info> wrote:
I DID in the discussion, immediately think of making an exception a dataclass, as someone else replied with. I guess if you want cargo in your exception, that's a pretty good way to do it.
"Cargo", that's an excellent term, thank you.
Before I used Python (and a bit overlapping), I largely developed in xBase languages (Clipper, FoxPro, XBase++, I even contributed slightly to a Free Software version called Harbour). In the later versions they added some OOP extensions that were used especially in relation to UI elements. The word "cargo" was in the official documentation of those languages to mean basically "optional data attached to a standard object." Basically, an extra runtime attribute that could be used for whatever. I think the word is really descriptive of a lot of what we do in many languages with "bundling some data with an object." I kinda wish it were used outside of those out-of-date languages. It's a good term. -- The dead increasingly dominate and strangle both the living and the not-yet born. Vampiric capital and undead corporate persons abuse the lives and control the thoughts of homo faber. Ideas, once born, become abortifacients against new conceptions.
On Mon, Oct 12, 2020, at 17:16, jmward01@gmail.com wrote:
Instead of needing a whole new class definition, wouldn't it be nice to just have something like:
.... #notice there isn't a boilerplate custom class created! try: if some_test_that_fails(variables): #I still have a base exception to fall back on for handlers that don't know my special exception raise Exception.my_special_exception(a, b, c, d, e, f) except Exception.my_special_excpetion(a:int, b:str, d, e, f): logger.warning(f"BAD THING {a} HAPPENED!") if not handle_it(a, b, c, f): raise
It seems like this could be a good use case for pattern matching... try: ... raise Exception(a, b, c, d, e, f) except Exception as e match e.args: case (a: int, b: str, c, _, _ f): logger.warning(f"BAD THING {a} HAPPENED!") if not handle_it(a, b, c, f): raise [Incidentally PEP 634 is very light on actual examples, and I'm having trouble visualizing what the syntax actually looks like, so please forgive me if I misunderstood what the pattern should look like]
participants (15)
-
Brian Allen Vanderburg II
-
Chris Angelico
-
Christopher Barker
-
David Mertz
-
Eric V. Smith
-
Greg Ewing
-
jmward01@gmail.com
-
M.-A. Lemburg
-
Random832
-
Richard Damon
-
Ronald Oussoren
-
Stephen J. Turnbull
-
Steven D'Aprano
-
Wes Turner
-
William Pickard